Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375const std = @import("std");const builtin = @import("builtin");const xev = @import("xev");const wuffs = @import("wuffs");const apprt = @import("../apprt.zig");const configpkg = @import("../config.zig");const font = @import("../font/main.zig");const inputpkg = @import("../input.zig");const os = @import("../os/main.zig");const terminal = @import("../terminal/main.zig");const renderer = @import("../renderer.zig");const math = @import("../math.zig");const Surface = @import("../Surface.zig");const link = @import("link.zig");const cellpkg = @import("cell.zig");const noMinContrast = cellpkg.noMinContrast;const constraintWidth = cellpkg.constraintWidth;const isCovering = cellpkg.isCovering;const rowNeverExtendBg = @import("row.zig").neverExtendBg;const Overlay = @import("Overlay.zig");const imagepkg = @import("image.zig");const ImageState = imagepkg.State;const shadertoy = @import("shadertoy.zig");const assert = @import("../quirks.zig").inlineAssert;const Allocator = std.mem.Allocator;const ArenaAllocator = std.heap.ArenaAllocator;const Terminal = terminal.Terminal;const Health = renderer.Health;
const getConstraint = @import("../font/nerd_font_attributes.zig").getConstraint;
const FileType = @import("../file_type.zig").FileType;
const macos = switch (builtin.os.tag) { .macos => @import("macos"), else => void,};
const DisplayLink = switch (builtin.os.tag) { .macos => *macos.video.DisplayLink, else => void,};
const log = std.log.scoped(.generic_renderer);
/// Create a renderer type with the provided graphics API wrapper.////// The graphics API wrapper must provide the interface outlined below./// Specific details for the interfaces are documented on the existing/// implementations (`Metal` and `OpenGL`).////// Hierarchy of graphics abstractions:////// [ GraphicsAPI ] - Responsible for configuring the runtime surface/// | | and providing render `Target`s that draw to it,/// | | as well as `Frame`s and `Pipeline`s./// | V/// | [ Target ] - Represents an abstract target for rendering, which/// | could be a surface directly but is also used as an/// | abstraction for off-screen frame buffers./// V/// [ Frame ] - Represents the context for drawing a given frame,/// | provides `RenderPass`es for issuing draw commands/// | to, and reports the frame health when complete./// V/// [ RenderPass ] - Represents a render pass in a frame, consisting of/// : one or more `Step`s applied to the same target(s),/// [ Step ] - - - - each describing the input buffers and textures and/// : the vertex/fragment functions and geometry to use./// :_ _ _ _ _ _ _ _ _ _//// v/// [ Pipeline ] - Describes a vertex and fragment function to be used/// for a `Step`; the `GraphicsAPI` is responsible for/// these and they should be constructed and cached/// ahead of time.////// [ Buffer ] - An abstraction over a GPU buffer.////// [ Texture ] - An abstraction over a GPU texture.///pub fn Renderer(comptime GraphicsAPI: type) type { return struct { const Self = @This();
pub const API = GraphicsAPI;
const Target = GraphicsAPI.Target; const Buffer = GraphicsAPI.Buffer; const Sampler = GraphicsAPI.Sampler; const Texture = GraphicsAPI.Texture; const RenderPass = GraphicsAPI.RenderPass;
const shaderpkg = GraphicsAPI.shaders; const Shaders = shaderpkg.Shaders;
/// Allocator that can be used alloc: std.mem.Allocator,
/// This mutex must be held whenever any state used in `drawFrame` is /// being modified, and also when it's being accessed in `drawFrame`. draw_mutex: std.Thread.Mutex = .{},
/// The configuration we need derived from the main config. config: DerivedConfig,
/// The mailbox for communicating with the window. surface_mailbox: apprt.surface.Mailbox,
/// Current font metrics defining our grid. grid_metrics: font.Metrics,
/// The size of everything. size: renderer.Size,
/// True if the window is focused focused: bool,
/// Flag to indicate that our focus state changed for custom /// shaders to update their state. custom_shader_focused_changed: bool = false,
/// The most recent scrollbar state. We use this as a cache to /// determine if we need to notify the apprt that there was a /// scrollbar change. scrollbar: terminal.Scrollbar, scrollbar_dirty: bool,
/// Tracks the last bottom-right pin of the screen to detect new output. /// When the final line changes (node or y differs), new content was added. /// Used for scroll-to-bottom on output feature. last_bottom_node: ?usize, last_bottom_y: terminal.size.CellCountInt,
/// The most recent viewport matches so that we can render search /// matches in the visible frame. This is provided asynchronously /// from the search thread so we have the dirty flag to also note /// if we need to rebuild our cells to include search highlights. /// /// Note that the selections MAY BE INVALID (point to PageList nodes /// that do not exist anymore). These must be validated prior to use. search_matches: ?renderer.Message.SearchMatches, search_selected_match: ?renderer.Message.SearchMatch, search_matches_dirty: bool,
/// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. cells: cellpkg.Contents,
/// Set to true after rebuildCells is called. This can be used /// to determine if any possible changes have been made to the /// cells for the draw call. cells_rebuilt: bool = false,
/// The current GPU uniform values. uniforms: shaderpkg.Uniforms,
/// Custom shader uniform values. custom_shader_uniforms: shadertoy.Uniforms,
/// Timestamp we rendered out first frame. /// /// This is used when updating custom shader uniforms. first_frame_time: ?std.time.Instant = null,
/// Timestamp when we rendered out more recent frame. /// /// This is used when updating custom shader uniforms. last_frame_time: ?std.time.Instant = null,
/// The font structures. font_grid: *font.SharedGrid, font_shaper: font.Shaper, font_shaper_cache: font.ShaperCache,
/// The images that we may render. images: ImageState = .empty,
/// Background image, if we have one. bg_image: ?imagepkg.Image = null, /// Set whenever the background image changes, signalling /// that the new background image needs to be uploaded to /// the GPU. /// /// This is initialized as true so that we load the image /// on renderer initialization, not just on config change. bg_image_changed: bool = true, /// Background image vertex buffer. bg_image_buffer: shaderpkg.BgImage, /// This value is used to force-update the swap chain copy /// of the background image buffer whenever we change it. bg_image_buffer_modified: usize = 0,
/// Graphics API state. api: GraphicsAPI,
/// The CVDisplayLink used to drive the rendering loop in /// sync with the display. This is void on platforms that /// don't support a display link. display_link: ?DisplayLink = null,
/// Health of the most recently completed frame. health: std.atomic.Value(Health) = .{ .raw = .healthy },
/// Our swap chain (multiple buffering) swap_chain: SwapChain,
/// This value is used to force-update swap chain targets in the /// event of a config change that requires it (such as blending mode). target_config_modified: usize = 0,
/// If something happened that requires us to reinitialize our shaders, /// this is set to true so that we can do that whenever possible. reinitialize_shaders: bool = false,
/// Whether or not we have custom shaders. has_custom_shaders: bool = false,
/// Our shader pipelines. shaders: Shaders,
/// The render state we update per loop. terminal_state: terminal.RenderState = .empty,
/// The number of frames since the last terminal state reset. /// We reset the terminal state after ~100,000 frames (about 10 to /// 15 minutes at 120Hz) to prevent wasted memory buildup from /// a large screen. terminal_state_frame_count: usize = 0,
/// Our overlay state, if any. overlay: ?Overlay = null,
const HighlightTag = enum(u8) { search_match, search_match_selected, }; /// Swap chain which maintains multiple copies of the state needed to /// render a frame, so that we can start building the next frame while /// the previous frame is still being processed on the GPU. const SwapChain = struct { // The count of buffers we use for double/triple buffering. // If this is one then we don't do any double+ buffering at all. // This is comptime because there isn't a good reason to change // this at runtime and there is a lot of complexity to support it. const buf_count = GraphicsAPI.swap_chain_count;
/// `buf_count` structs that can hold the /// data needed by the GPU to draw a frame. frames: [buf_count]FrameState, /// Index of the most recently used frame state struct. frame_index: std.math.IntFittingRange(0, buf_count) = 0, /// Semaphore that we wait on to make sure we have an available /// frame state struct so we can start working on a new frame. frame_sema: std.Thread.Semaphore = .{ .permits = buf_count },
/// Set to true when deinited, if you try to deinit a defunct /// swap chain it will just be ignored, to prevent double-free. /// /// This is required because of `displayUnrealized`, since it /// `deinits` the swapchain, which leads to a double-free if /// the renderer is deinited after that. defunct: bool = false,
pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain { var result: SwapChain = .{ .frames = undefined };
// Initialize all of our frame state. for (&result.frames) |*frame| { frame.* = try FrameState.init(api, custom_shaders); }
return result; }
pub fn deinit(self: *SwapChain) void { if (self.defunct) return; self.defunct = true;
// Wait for all of our inflight draws to complete // so that we can cleanly deinit our GPU state. for (0..buf_count) |_| self.frame_sema.wait(); for (&self.frames) |*frame| frame.deinit(); }
/// Get the next frame state to draw to. This will wait on the /// semaphore to ensure that the frame is available. This must /// always be paired with a call to releaseFrame. pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState { if (self.defunct) return error.Defunct;
self.frame_sema.wait(); errdefer self.frame_sema.post(); self.frame_index = (self.frame_index + 1) % buf_count; return &self.frames[self.frame_index]; }
/// This should be called when the frame has completed drawing. pub fn releaseFrame(self: *SwapChain) void { self.frame_sema.post(); } };
/// State we need duplicated for every frame. Any state that could be /// in a data race between the GPU and CPU while a frame is being drawn /// should be in this struct. /// /// While a draw is in-process, we "lock" the state (via a semaphore) /// and prevent the CPU from updating the state until our graphics API /// reports that the frame is complete. /// /// This is used to implement double/triple buffering. const FrameState = struct { uniforms: UniformBuffer, cells: CellTextBuffer, cells_bg: CellBgBuffer,
grayscale: Texture, grayscale_modified: usize = 0, color: Texture, color_modified: usize = 0,
target: Target, /// See property of same name on Renderer for explanation. target_config_modified: usize = 0,
/// Buffer with the vertex data for our background image. /// /// TODO: Make this an optional and only create it /// if we actually have a background image. bg_image_buffer: BgImageBuffer, /// See property of same name on Renderer for explanation. bg_image_buffer_modified: usize = 0,
/// Custom shader state, this is null if we have no custom shaders. custom_shader_state: ?CustomShaderState = null,
const UniformBuffer = Buffer(shaderpkg.Uniforms); const CellBgBuffer = Buffer(shaderpkg.CellBg); const CellTextBuffer = Buffer(shaderpkg.CellText); const BgImageBuffer = Buffer(shaderpkg.BgImage);
pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState { // Uniform buffer contains exactly 1 uniform struct. The // uniform data will be undefined so this must be set before // a frame is drawn. var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1); errdefer uniforms.deinit();
// Create GPU buffers for our cells. // // We start them off with a size of 1, which will of course be // too small, but they will be resized as needed. This is a bit // wasteful but since it's a one-time thing it's not really a // huge concern. var cells = try CellTextBuffer.init(api.fgBufferOptions(), 1); errdefer cells.deinit(); var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1); errdefer cells_bg.deinit();
// Create a GPU buffer for our background image info. var bg_image_buffer = try BgImageBuffer.init( api.bgImageBufferOptions(), 1, ); errdefer bg_image_buffer.deinit();
// Initialize our textures for our font atlas. // // As with the buffers above, we start these off as small // as possible since they'll inevitably be resized anyway. const grayscale = try api.initAtlasTexture(&.{ .data = undefined, .size = 1, .format = .grayscale, }); errdefer grayscale.deinit(); const color = try api.initAtlasTexture(&.{ .data = undefined, .size = 1, .format = .bgra, }); errdefer color.deinit();
var custom_shader_state = if (custom_shaders) try CustomShaderState.init(api) else null; errdefer if (custom_shader_state) |*state| state.deinit();
// Initialize the target. Just as with the other resources, // start it off as small as we can since it'll be resized. const target = try api.initTarget(1, 1);
return .{ .uniforms = uniforms, .cells = cells, .cells_bg = cells_bg, .bg_image_buffer = bg_image_buffer, .grayscale = grayscale, .color = color, .target = target, .custom_shader_state = custom_shader_state, }; }
pub fn deinit(self: *FrameState) void { self.target.deinit(); self.uniforms.deinit(); self.cells.deinit(); self.cells_bg.deinit(); self.grayscale.deinit(); self.color.deinit(); self.bg_image_buffer.deinit(); if (self.custom_shader_state) |*state| state.deinit(); }
pub fn resize( self: *FrameState, api: GraphicsAPI, width: usize, height: usize, ) !void { if (self.custom_shader_state) |*state| { try state.resize(api, width, height); } const target = try api.initTarget(width, height); self.target.deinit(); self.target = target; } };
/// State relevant to our custom shaders if we have any. const CustomShaderState = struct { /// When we have a custom shader state, we maintain a front /// and back texture which we use as a swap chain to render /// between when multiple custom shaders are defined. front_texture: Texture, back_texture: Texture,
/// Shadertoy uses a sampler for accessing the various channel /// textures. In Metal, we need to explicitly create these since /// the glslang-to-msl compiler doesn't do it for us (as we /// normally would in hand-written MSL). To keep it clean and /// consistent, we just force all rendering APIs to provide an /// explicit sampler. /// /// Samplers are immutable and describe sampling properties so /// we can share the sampler across front/back textures (although /// we only need it for the source texture at a time, we don't /// need to "swap" it). sampler: Sampler,
uniforms: UniformBuffer,
const UniformBuffer = Buffer(shadertoy.Uniforms);
/// Swap the front and back textures. pub fn swap(self: *CustomShaderState) void { std.mem.swap(Texture, &self.front_texture, &self.back_texture); }
pub fn init(api: GraphicsAPI) !CustomShaderState { // Create a GPU buffer to hold our uniforms. var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1); errdefer uniforms.deinit();
// Initialize the front and back textures at 1x1 px, this // is slightly wasteful but it's only done once so whatever. const front_texture = try Texture.init( api.textureOptions(), 1, 1, null, ); errdefer front_texture.deinit(); const back_texture = try Texture.init( api.textureOptions(), 1, 1, null, ); errdefer back_texture.deinit();
const sampler = try Sampler.init(api.samplerOptions()); errdefer sampler.deinit();
return .{ .front_texture = front_texture, .back_texture = back_texture, .sampler = sampler, .uniforms = uniforms, }; }
pub fn deinit(self: *CustomShaderState) void { self.front_texture.deinit(); self.back_texture.deinit(); self.sampler.deinit(); self.uniforms.deinit(); }
pub fn resize( self: *CustomShaderState, api: GraphicsAPI, width: usize, height: usize, ) !void { const front_texture = try Texture.init( api.textureOptions(), @intCast(width), @intCast(height), null, ); errdefer front_texture.deinit(); const back_texture = try Texture.init( api.textureOptions(), @intCast(width), @intCast(height), null, ); errdefer back_texture.deinit();
self.front_texture.deinit(); self.back_texture.deinit();
self.front_texture = front_texture; self.back_texture = back_texture; } };
/// The configuration for this renderer that is derived from the main /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { arena: ArenaAllocator,
font_thicken: bool, font_thicken_strength: u8, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, font_shaping_break: configpkg.FontShapingBreak, cursor_color: ?configpkg.Config.TerminalColor, cursor_opacity: f64, cursor_text: ?configpkg.Config.TerminalColor, background: terminal.color.RGB, background_opacity: f64, background_opacity_cells: bool, foreground: terminal.color.RGB, selection_background: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor, search_background: configpkg.Config.TerminalColor, search_foreground: configpkg.Config.TerminalColor, search_selected_background: configpkg.Config.TerminalColor, search_selected_foreground: configpkg.Config.TerminalColor, bold_color: ?terminal.Style.BoldColor, faint_opacity: u8, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, custom_shaders: configpkg.RepeatablePath, bg_image: ?configpkg.Path, bg_image_opacity: f32, bg_image_position: configpkg.BackgroundImagePosition, bg_image_fit: configpkg.BackgroundImageFit, bg_image_repeat: bool, links: link.Set, vsync: bool, colorspace: configpkg.Config.WindowColorspace, blending: configpkg.Config.AlphaBlending, background_blur: configpkg.Config.BackgroundBlur, scroll_to_bottom_on_output: bool,
pub fn init( alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { var arena = ArenaAllocator.init(alloc_gpa); errdefer arena.deinit(); const alloc = arena.allocator();
// Copy our shaders const custom_shaders = try config.@"custom-shader".clone(alloc);
// Copy our background image const bg_image = if (config.@"background-image") |bg| try bg.clone(alloc) else null;
// Copy our font features const font_features = try config.@"font-feature".clone(alloc);
// Get our font styles var font_styles = font.CodepointResolver.StyleStatus.initFill(true); font_styles.set(.bold, config.@"font-style-bold" != .false); font_styles.set(.italic, config.@"font-style-italic" != .false); font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
// Our link configs const links = try link.Set.fromConfig( alloc, config.link.links.items, );
return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .background_opacity_cells = config.@"background-opacity-cells", .font_thicken = config.@"font-thicken", .font_thicken_strength = config.@"font-thicken-strength", .font_features = font_features.list, .font_styles = font_styles, .font_shaping_break = config.@"font-shaping-break",
.cursor_color = config.@"cursor-color", .cursor_text = config.@"cursor-text", .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
.background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), .bold_color = if (config.@"bold-color") |b| b.toTerminal() else null, .faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)),
.min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color",
.selection_background = config.@"selection-background", .selection_foreground = config.@"selection-foreground", .search_background = config.@"search-background", .search_foreground = config.@"search-foreground", .search_selected_background = config.@"search-selected-background", .search_selected_foreground = config.@"search-selected-foreground",
.custom_shaders = custom_shaders, .bg_image = bg_image, .bg_image_opacity = config.@"background-image-opacity", .bg_image_position = config.@"background-image-position", .bg_image_fit = config.@"background-image-fit", .bg_image_repeat = config.@"background-image-repeat", .links = links, .vsync = config.@"window-vsync", .colorspace = config.@"window-colorspace", .blending = config.@"alpha-blending", .background_blur = config.@"background-blur", .scroll_to_bottom_on_output = config.@"scroll-to-bottom".output, .arena = arena, }; }
pub fn deinit(self: *DerivedConfig) void { const alloc = self.arena.allocator(); self.links.deinit(alloc); self.arena.deinit(); } };
pub fn init(alloc: Allocator, options: renderer.Options) !Self { // Initialize our graphics API wrapper, this will prepare the // surface provided by the apprt and set up any API-specific // GPU resources. var api = try GraphicsAPI.init(alloc, options); errdefer api.deinit();
const has_custom_shaders = options.config.custom_shaders.value.items.len > 0;
// Prepare our swap chain var swap_chain = try SwapChain.init( api, has_custom_shaders, ); errdefer swap_chain.deinit();
// Create the font shaper. var font_shaper = try font.Shaper.init(alloc, .{ .features = options.config.font_features.items, }); errdefer font_shaper.deinit();
// Initialize all the data that requires a critical font section. const font_critical: struct { metrics: font.Metrics, } = font_critical: { const grid: *font.SharedGrid = options.font_grid; grid.lock.lockShared(); defer grid.lock.unlockShared(); break :font_critical .{ .metrics = grid.metrics, }; };
const display_link: ?DisplayLink = switch (builtin.os.tag) { .macos => if (options.config.vsync) try macos.video.DisplayLink.createWithActiveCGDisplays() else null, else => null, }; errdefer if (display_link) |v| v.release();
var result: Self = .{ .alloc = alloc, .config = options.config, .surface_mailbox = options.surface_mailbox, .grid_metrics = font_critical.metrics, .size = options.size, .focused = true, .scrollbar = .zero, .scrollbar_dirty = false, .last_bottom_node = null, .last_bottom_y = 0, .search_matches = null, .search_selected_match = null, .search_matches_dirty = false,
// Render state .cells = .{}, .uniforms = .{ .projection_matrix = undefined, .cell_size = undefined, .grid_size = undefined, .grid_padding = undefined, .screen_size = undefined, .padding_extend = .{}, .min_contrast = options.config.min_contrast, .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, .cursor_color = undefined, .bg_color = .{ options.config.background.r, options.config.background.g, options.config.background.b, // Note that if we're on macOS with glass effects // we'll disable background opacity but we handle // that in updateFrame. @intFromFloat(@round(options.config.background_opacity * 255.0)), }, .bools = .{ .cursor_wide = false, .use_display_p3 = options.config.colorspace == .@"display-p3", .use_linear_blending = options.config.blending.isLinear(), .use_linear_correction = options.config.blending == .@"linear-corrected", }, }, .custom_shader_uniforms = .{ .resolution = .{ 0, 0, 1 }, .time = 0, .time_delta = 0, .frame_rate = 60, // not currently updated .frame = 0, .channel_time = @splat(@splat(0)), // not currently updated .channel_resolution = @splat(@splat(0)), .mouse = @splat(0), // not currently updated .date = @splat(0), // not currently updated .sample_rate = 0, // N/A, we don't have any audio .current_cursor = @splat(0), .previous_cursor = @splat(0), .current_cursor_color = @splat(0), .previous_cursor_color = @splat(0), .current_cursor_style = 0, .previous_cursor_style = 0, .cursor_visible = 0, .cursor_change_time = 0, .time_focus = 0, .focus = 1, // assume focused initially .palette = @splat(@splat(0)), .background_color = @splat(0), .foreground_color = @splat(0), .cursor_color = @splat(0), .cursor_text = @splat(0), .selection_background_color = @splat(0), .selection_foreground_color = @splat(0), }, .bg_image_buffer = undefined,
// Fonts .font_grid = options.font_grid, .font_shaper = font_shaper, .font_shaper_cache = font.ShaperCache.init(),
// Shaders (initialized below) .shaders = undefined,
// Graphics API stuff .api = api, .swap_chain = swap_chain, .display_link = display_link, };
try result.initShaders();
// Ensure our undefined values above are correctly initialized. result.updateFontGridUniforms(); result.updateScreenSizeUniforms(); result.updateBgImageBuffer(); try result.prepBackgroundImage();
return result; }
pub fn deinit(self: *Self) void { if (self.overlay) |*overlay| overlay.deinit(self.alloc); self.terminal_state.deinit(self.alloc); if (self.search_selected_match) |*m| m.arena.deinit(); if (self.search_matches) |*m| m.arena.deinit(); self.swap_chain.deinit();
if (DisplayLink != void) { if (self.display_link) |display_link| { display_link.stop() catch {}; display_link.release(); } }
self.cells.deinit(self.alloc);
self.font_shaper.deinit(); self.font_shaper_cache.deinit(self.alloc);
self.config.deinit();
self.images.deinit(self.alloc);
if (self.bg_image) |img| img.deinit(self.alloc);
self.deinitShaders();
self.api.deinit();
self.* = undefined; }
fn deinitShaders(self: *Self) void { self.shaders.deinit(self.alloc); }
fn initShaders(self: *Self) !void { var arena = ArenaAllocator.init(self.alloc); defer arena.deinit(); const arena_alloc = arena.allocator();
// Load our custom shaders const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( arena_alloc, self.config.custom_shaders, GraphicsAPI.custom_shader_target, ) catch |err| err: { log.warn("error loading custom shaders err={}", .{err}); break :err &.{}; };
const has_custom_shaders = custom_shaders.len > 0;
var shaders = try self.api.initShaders( self.alloc, custom_shaders, ); errdefer shaders.deinit(self.alloc);
self.shaders = shaders; self.has_custom_shaders = has_custom_shaders; }
/// This is called early right after surface creation. pub fn surfaceInit(surface: *apprt.Surface) !void { // If our API has to do things here, let it. if (@hasDecl(GraphicsAPI, "surfaceInit")) { try GraphicsAPI.surfaceInit(surface); } }
/// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void { // If our API has to do things to finalize surface init, let it. if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) { try self.api.finalizeSurfaceInit(surface); } }
/// Callback called by renderer.Thread when it begins. pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void { // If our API has to do things on thread enter, let it. if (@hasDecl(GraphicsAPI, "threadEnter")) { try self.api.threadEnter(surface); } }
/// Callback called by renderer.Thread when it exits. pub fn threadExit(self: *const Self) void { // If our API has to do things on thread exit, let it. if (@hasDecl(GraphicsAPI, "threadExit")) { self.api.threadExit(); } }
/// Called by renderer.Thread when it starts the main loop. pub fn loopEnter(self: *Self, thr: *renderer.Thread) !void { // If our API has to do things on loop enter, let it. if (@hasDecl(GraphicsAPI, "loopEnter")) { self.api.loopEnter(); }
// If we don't support a display link we have no work to do. if (comptime DisplayLink == void) return;
// This is when we know our "self" pointer is stable so we can // setup the display link. To setup the display link we set our // callback and we can start it immediately. const display_link = self.display_link orelse return; try display_link.setOutputCallback( xev.Async, &displayLinkCallback, &thr.draw_now, ); display_link.start() catch {}; }
/// Called by renderer.Thread when it exits the main loop. pub fn loopExit(self: *Self) void { // If our API has to do things on loop exit, let it. if (@hasDecl(GraphicsAPI, "loopExit")) { self.api.loopExit(); }
// If we don't support a display link we have no work to do. if (comptime DisplayLink == void) return;
// Stop our display link. If this fails its okay it just means // that we either never started it or the view its attached to // is gone which is fine. const display_link = self.display_link orelse return; display_link.stop() catch {}; }
/// This is called by the GTK apprt after the surface is /// reinitialized due to any of the events mentioned in /// the doc comment for `displayUnrealized`. pub fn displayRealized(self: *Self) !void { // If our API has to do things on realize, let it. if (@hasDecl(GraphicsAPI, "displayRealized")) { self.api.displayRealized(); }
// Lock the draw mutex so that we can // safely reinitialize our GPU resources. self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// We assume that the swap chain was deinited in // `displayUnrealized`, in which case it should be // marked defunct. If not, we have a problem. assert(self.swap_chain.defunct);
// We reinitialize our shaders and our swap chain. try self.initShaders(); self.swap_chain = try SwapChain.init( self.api, self.has_custom_shaders, ); self.reinitialize_shaders = false; self.target_config_modified = 1; }
/// This is called by the GTK apprt when the surface is being destroyed. /// This can happen because the surface is being closed but also when /// moving the window between displays or splitting. pub fn displayUnrealized(self: *Self) void { // If our API has to do things on unrealize, let it. if (@hasDecl(GraphicsAPI, "displayUnrealized")) { self.api.displayUnrealized(); }
// Lock the draw mutex so that we can // safely deinitialize our GPU resources. self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// We deinit our swap chain and shaders. // // This will mark them as defunct so that they // can't be double-freed or used in draw calls. self.swap_chain.deinit(); self.shaders.deinit(self.alloc); }
fn displayLinkCallback( _: *macos.video.DisplayLink, ud: ?*xev.Async, ) void { const draw_now = ud orelse return; draw_now.notify() catch |err| { log.err("error notifying draw_now err={}", .{err}); }; }
/// Mark the full screen as dirty so that we redraw everything. pub inline fn markDirty(self: *Self) void { self.terminal_state.dirty = .full; }
/// Called when we get an updated display ID for our display link. pub fn setMacOSDisplayID(self: *Self, id: u32) !void { if (comptime DisplayLink == void) return; const display_link = self.display_link orelse return; log.info("updating display link display id={}", .{id}); display_link.setCurrentCGDisplay(id) catch |err| { log.warn("error setting display link display id err={}", .{err}); }; }
/// True if our renderer has animations so that a higher frequency /// timer is used. pub fn hasAnimations(self: *const Self) bool { return self.has_custom_shaders; }
/// True if our renderer is using vsync. If true, the renderer or apprt /// is responsible for triggering draw_now calls to the render thread. /// That is the only way to trigger a drawFrame. pub fn hasVsync(self: *const Self) bool { if (comptime DisplayLink == void) return false; const display_link = self.display_link orelse return false; return display_link.isRunning(); }
/// Callback when the focus changes for the terminal this is rendering. /// /// Must be called on the render thread. pub fn setFocus(self: *Self, focus: bool) !void { assert(self.focused != focus);
self.focused = focus;
// Flag that we need to update our custom shaders self.custom_shader_focused_changed = true;
// If we're not focused, then we want to stop the display link // because it is a waste of resources and we can move to pure // change-driven updates. if (comptime DisplayLink != void) link: { const display_link = self.display_link orelse break :link; if (focus) { display_link.start() catch {}; } else { display_link.stop() catch {}; } } }
/// Callback when the window is visible or occluded. /// /// Must be called on the render thread. pub fn setVisible(self: *Self, visible: bool) void { // If we're not visible, then we want to stop the display link // because it is a waste of resources and we can move to pure // change-driven updates. if (comptime DisplayLink != void) link: { const display_link = self.display_link orelse break :link; if (visible and self.focused) { display_link.start() catch {}; } else { display_link.stop() catch {}; } } }
/// Set the new font grid. /// /// Must be called on the render thread. pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void { self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// Update our grid self.font_grid = grid;
// Update all our textures so that they sync on the next frame. // We can modify this without a lock because the GPU does not // touch this data. for (&self.swap_chain.frames) |*frame| { frame.grayscale_modified = 0; frame.color_modified = 0; }
// Get our metrics from the grid. This doesn't require a lock because // the metrics are never recalculated. const metrics = grid.metrics; self.grid_metrics = metrics;
// Reset our shaper cache. If our font changed (not just the size) then // the data in the shaper cache may be invalid and cannot be used, so we // always clear the cache just in case. const font_shaper_cache = font.ShaperCache.init(); self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache;
// Update cell size. self.size.cell = .{ .width = metrics.cell_width, .height = metrics.cell_height, };
// Update relevant uniforms self.updateFontGridUniforms();
// Force a full rebuild, because cached rows may still reference // an outdated atlas from the old grid and this can cause garbage // to be rendered. self.markDirty(); }
/// Update uniforms that are based on the font grid. /// /// Caller must hold the draw mutex. fn updateFontGridUniforms(self: *Self) void { self.uniforms.cell_size = .{ @floatFromInt(self.grid_metrics.cell_width), @floatFromInt(self.grid_metrics.cell_height), }; }
/// Update the frame data. pub fn updateFrame( self: *Self, state: *renderer.State, cursor_blink_visible: bool, ) Allocator.Error!void { // const start = std.time.Instant.now() catch unreachable; // const start_micro = std.time.microTimestamp(); // defer { // const end = std.time.Instant.now() catch unreachable; // log.warn( // "[updateFrame time] start_micro={} duration={}ns", // .{ start_micro, end.since(start) / std.time.ns_per_us }, // ); // }
// We fully deinit and reset the terminal state every so often // so that a particularly large terminal state doesn't cause // the renderer to hold on to retained memory. // // Frame count is ~12 minutes at 120Hz. const max_terminal_state_frame_count = 100_000; if (self.terminal_state_frame_count >= max_terminal_state_frame_count) { self.terminal_state.deinit(self.alloc); self.terminal_state = .empty; } self.terminal_state_frame_count += 1;
// Create an arena for all our temporary allocations while rebuilding var arena = ArenaAllocator.init(self.alloc); defer arena.deinit(); const arena_alloc = arena.allocator();
// Data we extract out of the critical area. const Critical = struct { links: terminal.RenderState.CellSet, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, scrollbar: terminal.Scrollbar, overlay_features: []const Overlay.Feature, };
// Update all our data as tightly as possible within the mutex. var critical: Critical = critical: { // const start = try std.time.Instant.now(); // const start_micro = std.time.microTimestamp(); // defer { // const end = std.time.Instant.now() catch unreachable; // std.log.err("[updateFrame critical time] start={}\tduration={} us", .{ start_micro, end.since(start) / std.time.ns_per_us }); // }
state.mutex.lock(); defer state.mutex.unlock();
// If we're in a synchronized output state, we pause all rendering. if (state.terminal.modes.get(.synchronized_output)) { log.debug("synchronized output started, skipping render", .{}); return; }
// If scroll-to-bottom on output is enabled, check if the final line // changed by comparing the bottom-right pin. If the node pointer or // y offset changed, new content was added to the screen. // Update this BEFORE we update our render state so we can // draw the new scrolled data immediately. if (self.config.scroll_to_bottom_on_output) scroll: { const br = state.terminal.screens.active.pages.getBottomRight(.screen) orelse break :scroll;
// If the pin hasn't changed, then don't scroll. if (self.last_bottom_node == @intFromPtr(br.node) and self.last_bottom_y == br.y) break :scroll;
// Update tracked pin state for next frame self.last_bottom_node = @intFromPtr(br.node); self.last_bottom_y = br.y;
// Scroll state.terminal.scrollViewport(.bottom); }
// Update our terminal state try self.terminal_state.update(self.alloc, state.terminal);
// If our terminal state is dirty at all we need to redo // the viewport search. if (self.terminal_state.dirty != .false) { state.terminal.flags.search_viewport_dirty = true; }
// Get our scrollbar out of the terminal. We synchronize // the scrollbar read with frame data updates because this // naturally limits the number of calls to this method (it // 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();
// Get our preedit state const preedit: ?renderer.State.Preedit = preedit: { const p = state.preedit orelse break :preedit null; break :preedit try p.clone(arena_alloc); };
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if // it changes. // // If we have any virtual references, we must also rebuild our // kitty state on every frame because any cell change can move // an image. if (self.images.kittyRequiresUpdate(state.terminal)) { // We need to grab the draw mutex since this updates // our image state that drawFrame uses. self.draw_mutex.lock(); defer self.draw_mutex.unlock(); self.images.kittyUpdate( self.alloc, state.terminal, .{ .width = self.grid_metrics.cell_width, .height = self.grid_metrics.cell_height, }, ); }
// Get our OSC8 links we're hovering if we have a mouse. // This requires terminal state because of URLs. const links: terminal.RenderState.CellSet = osc8: { // If our mouse isn't hovering, we have no links. const vp = state.mouse.point orelse break :osc8 .empty;
// If the right mods aren't pressed, then we can't match. if (!state.mouse.mods.equal(inputpkg.ctrlOrSuper(.{}))) break :osc8 .empty;
break :osc8 self.terminal_state.linkCells( arena_alloc, vp, ) catch |err| { log.warn("error searching for OSC8 links err={}", .{err}); break :osc8 .empty; }; };
const overlay_features: []const Overlay.Feature = overlay: { const insp = state.inspector orelse break :overlay &.{}; const renderer_info = insp.rendererInfo(); break :overlay renderer_info.overlayFeatures( arena_alloc, ) catch &.{}; };
break :critical .{ .links = links, .mouse = state.mouse, .preedit = preedit, .scrollbar = scrollbar, .overlay_features = overlay_features, }; };
// Outside the critical area we can update our links to contain // our regex results. self.config.links.renderCellMap( arena_alloc, &critical.links, &self.terminal_state, state.mouse.point, state.mouse.mods, ) catch |err| { log.warn("error searching for regex links err={}", .{err}); };
// Clear our highlight state and update. if (self.search_matches_dirty or self.terminal_state.dirty != .false) { self.search_matches_dirty = false;
// Clear the prior highlights const row_data = self.terminal_state.row_data.slice(); var any_dirty: bool = false; for ( row_data.items(.highlights), row_data.items(.dirty), ) |*highlights, *dirty| { if (highlights.items.len > 0) { highlights.clearRetainingCapacity(); dirty.* = true; any_dirty = true; } } if (any_dirty and self.terminal_state.dirty == .false) { self.terminal_state.dirty = .partial; }
// NOTE: The order below matters. Highlights added earlier // will take priority.
if (self.search_selected_match) |m| { self.terminal_state.updateHighlightsFlattened( self.alloc, @intFromEnum(HighlightTag.search_match_selected), &.{m.match}, ) catch |err| { // Not a critical error, we just won't show highlights. log.warn("error updating search selected highlight err={}", .{err}); }; }
if (self.search_matches) |m| { self.terminal_state.updateHighlightsFlattened( self.alloc, @intFromEnum(HighlightTag.search_match), m.matches, ) catch |err| { // Not a critical error, we just won't show highlights. log.warn("error updating search highlights err={}", .{err}); }; } }
// From this point forward no more errors. errdefer comptime unreachable;
// Reset our dirty state after updating. defer self.terminal_state.dirty = .false;
// Rebuild the overlay image if we have one. We can do this // outside of any critical areas. self.rebuildOverlay( critical.overlay_features, ) catch |err| { log.warn( "error rebuilding overlay surface err={}", .{err}, ); };
// Acquire the draw mutex for all remaining state updates. { self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// Build our GPU cells self.rebuildCells( critical.preedit, renderer.cursorStyle(&self.terminal_state, .{ .preedit = critical.preedit != null, .focused = self.focused, .blink_visible = cursor_blink_visible, }), &critical.links, ) catch |err| { // This means we weren't able to allocate our buffer // to update the cells. In this case, we continue with // our old buffer (frozen contents) and log it. comptime assert(@TypeOf(err) == error{OutOfMemory}); log.warn("error rebuilding GPU cells err={}", .{err}); };
// The scrollbar is only emitted during draws so we also // check the scrollbar cache here and update if needed. // This is pretty fast. if (!self.scrollbar.eql(critical.scrollbar)) { self.scrollbar = critical.scrollbar; self.scrollbar_dirty = true; }
// Update our background color self.uniforms.bg_color = .{ self.terminal_state.colors.background.r, self.terminal_state.colors.background.g, self.terminal_state.colors.background.b, @intFromFloat(@round(self.config.background_opacity * 255.0)), };
// If we're on macOS and have glass styles, we remove // the background opacity because the glass effect handles // it. if (comptime builtin.os.tag == .macos) switch (self.config.background_blur) { .@"macos-glass-regular", .@"macos-glass-clear", => self.uniforms.bg_color[3] = 0,
else => {}, };
// Prepare our overlay image for upload (or unload). This // has to use our general allocator since it modifies // state that survives frames. self.images.overlayUpdate( self.alloc, self.overlay, ) catch |err| { log.warn("error updating overlay images err={}", .{err}); };
// Update custom shader uniforms that depend on terminal state. self.updateCustomShaderUniformsFromState(); }
// Notify our shaper we're done for the frame. For some shapers, // such as CoreText, this triggers off-thread cleanup logic. self.font_shaper.endFrame(); }
/// Draw the frame to the screen. /// /// If `sync` is true, this will synchronously block until /// the frame is finished drawing and has been presented. pub fn drawFrame( self: *Self, sync: bool, ) !void { // const start = std.time.Instant.now() catch unreachable; // const start_micro = std.time.microTimestamp(); // defer { // const end = std.time.Instant.now() catch unreachable; // log.warn( // "[drawFrame time] start_micro={} duration={}ns", // .{ start_micro, end.since(start) / std.time.ns_per_us }, // ); // }
// We hold a the draw mutex to prevent changes to any // data we access while we're in the middle of drawing. self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// After the graphics API is complete (so we defer) we want to // update our scrollbar state. defer if (self.scrollbar_dirty) { // Fail instantly if the surface mailbox if full, we'll just // get it on the next frame. if (self.surface_mailbox.push(.{ .scrollbar = self.scrollbar, }, .instant) > 0) self.scrollbar_dirty = false; };
// Let our graphics API do any bookkeeping, etc. // that it needs to do before / after `drawFrame`. self.api.drawFrameStart(); defer self.api.drawFrameEnd();
// Retrieve the most up-to-date surface size from the Graphics API const surface_size = try self.api.surfaceSize();
// If either of our surface dimensions is zero // then drawing is absurd, so we just return. if (surface_size.width == 0 or surface_size.height == 0) return;
const size_changed = self.size.screen.width != surface_size.width or self.size.screen.height != surface_size.height;
// Conditions under which we need to draw the frame, otherwise we // don't need to since the previous frame should be identical. const needs_redraw = size_changed or self.cells_rebuilt or self.hasAnimations() or sync;
if (!needs_redraw) { // We still need to present the last target again, because the // apprt may be swapping buffers and display an outdated frame // if we don't draw something new. try self.api.presentLastTarget(); return; } self.cells_rebuilt = false;
// Wait for a frame to be available. const frame = try self.swap_chain.nextFrame(); errdefer self.swap_chain.releaseFrame(); // log.debug("drawing frame index={}", .{self.swap_chain.frame_index});
// If we need to reinitialize our shaders, do so. if (self.reinitialize_shaders) { self.reinitialize_shaders = false; self.shaders.deinit(self.alloc); try self.initShaders(); }
// Our shaders should not be defunct at this point. assert(!self.shaders.defunct);
// If we have custom shaders, make sure we have the // custom shader state in our frame state, otherwise // if we have a state but don't need it we remove it. if (self.has_custom_shaders) { if (frame.custom_shader_state == null) { frame.custom_shader_state = try .init(self.api); try frame.custom_shader_state.?.resize( self.api, surface_size.width, surface_size.height, ); } } else if (frame.custom_shader_state) |*state| { state.deinit(); frame.custom_shader_state = null; }
// If our stored size doesn't match the // surface size we need to update it. if (size_changed) { self.size.screen = .{ .width = surface_size.width, .height = surface_size.height, }; self.updateScreenSizeUniforms(); }
// If this frame's target isn't the correct size, or the target // config has changed (such as when the blending mode changes), // remove it and replace it with a new one with the right values. if (frame.target.width != self.size.screen.width or frame.target.height != self.size.screen.height or frame.target_config_modified != self.target_config_modified) { try frame.resize( self.api, self.size.screen.width, self.size.screen.height, ); frame.target_config_modified = self.target_config_modified; }
// Upload images to the GPU as necessary. _ = self.images.upload(self.alloc, &self.api);
// Upload the background image to the GPU as necessary. try self.uploadBackgroundImage();
// Update per-frame custom shader uniforms. try self.updateCustomShaderUniformsForFrame();
// Setup our frame data try frame.uniforms.sync(&.{self.uniforms}); try frame.cells_bg.sync(self.cells.bg_cells); const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
// If our background image buffer has changed, sync it. if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) { try frame.bg_image_buffer.sync(&.{self.bg_image_buffer});
frame.bg_image_buffer_modified = self.bg_image_buffer_modified; }
// If our font atlas changed, sync the texture data texture: { const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); if (modified <= frame.grayscale_modified) break :texture; self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale); } texture: { const modified = self.font_grid.atlas_color.modified.load(.monotonic); if (modified <= frame.color_modified) break :texture; self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color); }
// Get a frame context from the graphics API. var frame_ctx = try self.api.beginFrame(self, &frame.target); defer frame_ctx.complete(sync);
{ var pass = frame_ctx.renderPass(&.{.{ .target = if (frame.custom_shader_state) |state| .{ .texture = state.back_texture } else .{ .target = frame.target }, .clear_color = .{ 0.0, 0.0, 0.0, 0.0 }, }}); defer pass.complete();
// First we draw our background image, if we have one. // The bg image shader also draws the main bg color. // // Otherwise, if we don't have a background image, we // draw the background color by itself in its own step. // // NOTE: We don't use the clear_color for this because that // would require us to do color space conversion on the // CPU-side. In the future when we have utilities for // that we should remove this step and use clear_color. if (self.bg_image) |img| switch (img) { .ready => |texture| pass.step(.{ .pipeline = self.shaders.pipelines.bg_image, .uniforms = frame.uniforms.buffer, .buffers = &.{frame.bg_image_buffer.buffer}, .textures = &.{texture}, .draw = .{ .type = .triangle, .vertex_count = 3 }, }), else => {}, } else { pass.step(.{ .pipeline = self.shaders.pipelines.bg_color, .uniforms = frame.uniforms.buffer, .buffers = &.{ null, frame.cells_bg.buffer }, .draw = .{ .type = .triangle, .vertex_count = 3 }, }); }
// Then we draw any kitty images that need // to be behind text AND cell backgrounds. self.images.draw( &self.api, self.shaders.pipelines.image, &pass, .kitty_below_bg, );
// Then we draw any opaque cell backgrounds. pass.step(.{ .pipeline = self.shaders.pipelines.cell_bg, .uniforms = frame.uniforms.buffer, .buffers = &.{ null, frame.cells_bg.buffer }, .draw = .{ .type = .triangle, .vertex_count = 3 }, });
// Kitty images between cell backgrounds and text. self.images.draw( &self.api, self.shaders.pipelines.image, &pass, .kitty_below_text, );
// Text. pass.step(.{ .pipeline = self.shaders.pipelines.cell_text, .uniforms = frame.uniforms.buffer, .buffers = &.{ frame.cells.buffer, frame.cells_bg.buffer, }, .textures = &.{ frame.grayscale, frame.color, }, .draw = .{ .type = .triangle_strip, .vertex_count = 4, .instance_count = fg_count, }, });
// Kitty images in front of text. self.images.draw( &self.api, self.shaders.pipelines.image, &pass, .kitty_above_text, );
// Debug overlay. We do this before any custom shader state // because our debug overlay is aligned with the grid. if (self.overlay != null) self.images.draw( &self.api, self.shaders.pipelines.image, &pass, .overlay, ); }
// If we have custom shaders, then we render them. if (frame.custom_shader_state) |*state| { // Sync our uniforms. try state.uniforms.sync(&.{self.custom_shader_uniforms});
for (self.shaders.post_pipelines, 0..) |pipeline, i| { defer state.swap();
var pass = frame_ctx.renderPass(&.{.{ .target = if (i < self.shaders.post_pipelines.len - 1) .{ .texture = state.front_texture } else .{ .target = frame.target }, .clear_color = .{ 0.0, 0.0, 0.0, 0.0 }, }}); defer pass.complete();
pass.step(.{ .pipeline = pipeline, .uniforms = state.uniforms.buffer, .textures = &.{state.back_texture}, .samplers = &.{state.sampler}, .draw = .{ .type = .triangle, .vertex_count = 3, }, }); } } }
// Callback from the graphics API when a frame is completed. pub fn frameCompleted( self: *Self, health: Health, ) void { // If our health value hasn't changed, then we do nothing. We don't // do a cmpxchg here because strict atomicity isn't important. if (self.health.load(.seq_cst) != health) { self.health.store(health, .seq_cst);
// Our health value changed, so we notify the surface so that it // can do something about it. _ = self.surface_mailbox.push(.{ .renderer_health = health, }, .{ .forever = {} }); }
// Always release our semaphore self.swap_chain.releaseFrame(); }
/// Call this any time the background image path changes. /// /// Caller must hold the draw mutex. fn prepBackgroundImage(self: *Self) !void { // Then we try to load the background image if we have a path. if (self.config.bg_image) |p| load_background: { const path = switch (p) { .required, .optional => |slice| slice, };
// Open the file var file = std.fs.openFileAbsolute(path, .{}) catch |err| { log.warn( "error opening background image file \"{s}\": {}", .{ path, err }, ); break :load_background; }; defer file.close();
// Read it const contents = file.readToEndAlloc( self.alloc, std.math.maxInt(u32), // Max size of 4 GiB, for now. ) catch |err| { log.warn( "error reading background image file \"{s}\": {}", .{ path, err }, ); break :load_background; }; defer self.alloc.free(contents);
// Figure out what type it probably is. const file_type = switch (FileType.detect(contents)) { .unknown => FileType.guessFromExtension( std.fs.path.extension(path), ), else => |t| t, };
// Decode it if we know how. const image_data = switch (file_type) { .png => try wuffs.png.decode(self.alloc, contents), .jpeg => try wuffs.jpeg.decode(self.alloc, contents), .unknown => { log.warn( "Cannot determine file type for background image file \"{s}\"!", .{path}, ); break :load_background; }, else => |f| { log.warn( "Unsupported file type {} for background image file \"{s}\"!", .{ f, path }, ); break :load_background; }, };
const image: imagepkg.Image = .{ .pending = .{ .width = image_data.width, .height = image_data.height, .pixel_format = .rgba, .data = image_data.data.ptr, }, };
// If we have an existing background image, replace it. // Otherwise, set this as our background image directly. if (self.bg_image) |*img| { img.markForReplace(self.alloc, image); } else { self.bg_image = image; } } else { // If we don't have a background image path, mark our // background image for unload if we currently have one. if (self.bg_image) |*img| img.markForUnload(); } }
fn uploadBackgroundImage(self: *Self) !void { // Make sure our bg image is uploaded if it needs to be. if (self.bg_image) |*bg| { if (bg.isUnloading()) { bg.deinit(self.alloc); self.bg_image = null; return; } if (bg.isPending()) try bg.upload(self.alloc, &self.api); } }
/// Update the configuration. pub fn changeConfig(self: *Self, config: *DerivedConfig) !void { self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// We always redo the font shaper in case font features changed. We // could check to see if there was an actual config change but this is // easier and rare enough to not cause performance issues. { var font_shaper = try font.Shaper.init(self.alloc, .{ .features = config.font_features.items, }); errdefer font_shaper.deinit(); self.font_shaper.deinit(); self.font_shaper = font_shaper; }
// We also need to reset the shaper cache so shaper info // from the previous font isn't reused for the new font. const font_shaper_cache = font.ShaperCache.init(); self.font_shaper_cache.deinit(self.alloc); self.font_shaper_cache = font_shaper_cache;
// Set our new minimum contrast self.uniforms.min_contrast = config.min_contrast;
// Set our new color space and blending self.uniforms.bools.use_display_p3 = config.colorspace == .@"display-p3"; self.uniforms.bools.use_linear_blending = config.blending.isLinear(); self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected";
const bg_image_config_changed = self.config.bg_image_fit != config.bg_image_fit or self.config.bg_image_position != config.bg_image_position or self.config.bg_image_repeat != config.bg_image_repeat or self.config.bg_image_opacity != config.bg_image_opacity;
const bg_image_changed = if (self.config.bg_image) |old| if (config.bg_image) |new| !old.equal(new) else true else config.bg_image != null;
const old_blending = self.config.blending; const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
self.config.deinit(); self.config = config.*;
// If our background image path changed, prepare the new bg image. if (bg_image_changed) try self.prepBackgroundImage();
// If our background image config changed, update the vertex buffer. if (bg_image_config_changed) self.updateBgImageBuffer();
// Reset our viewport to force a rebuild, in case of a font change. self.markDirty();
const blending_changed = old_blending != config.blending;
if (blending_changed) { // We update our API's blending mode. self.api.blending = config.blending; // And indicate that we need to reinitialize our shaders. self.reinitialize_shaders = true; // And indicate that our swap chain targets need to // be re-created to account for the new blending mode. self.target_config_modified +%= 1; }
if (custom_shaders_changed) { self.reinitialize_shaders = true; } }
/// Resize the screen. pub fn setScreenSize( self: *Self, size: renderer.Size, ) void { self.draw_mutex.lock(); defer self.draw_mutex.unlock();
// We only actually need the padding from this, // everything else is derived elsewhere. self.size.padding = size.padding;
self.updateScreenSizeUniforms();
log.debug("screen size size={}", .{size}); }
/// Update uniforms that are based on the screen size. /// /// Caller must hold the draw mutex. fn updateScreenSizeUniforms(self: *Self) void { const terminal_size = self.size.terminal();
// Blank space around the grid. const blank: renderer.Padding = self.size.screen.blankPadding( self.size.padding, .{ .columns = self.cells.size.columns, .rows = self.cells.size.rows, }, .{ .width = self.grid_metrics.cell_width, .height = self.grid_metrics.cell_height, }, ).add(self.size.padding);
// Setup our uniforms self.uniforms.projection_matrix = math.ortho2d( -1 * @as(f32, @floatFromInt(self.size.padding.left)), @floatFromInt(terminal_size.width + self.size.padding.right), @floatFromInt(terminal_size.height + self.size.padding.bottom), -1 * @as(f32, @floatFromInt(self.size.padding.top)), ); self.uniforms.grid_padding = .{ @floatFromInt(blank.top), @floatFromInt(blank.right), @floatFromInt(blank.bottom), @floatFromInt(blank.left), }; self.uniforms.screen_size = .{ @floatFromInt(self.size.screen.width), @floatFromInt(self.size.screen.height), }; }
/// Update the background image vertex buffer (CPU-side). /// /// This should be called if and when configs change that /// could affect the background image. /// /// Caller must hold the draw mutex. fn updateBgImageBuffer(self: *Self) void { self.bg_image_buffer = .{ .opacity = self.config.bg_image_opacity, .info = .{ .position = switch (self.config.bg_image_position) { .@"top-left" => .tl, .@"top-center" => .tc, .@"top-right" => .tr, .@"center-left" => .ml, .@"center-center", .center => .mc, .@"center-right" => .mr, .@"bottom-left" => .bl, .@"bottom-center" => .bc, .@"bottom-right" => .br, }, .fit = switch (self.config.bg_image_fit) { .contain => .contain, .cover => .cover, .stretch => .stretch, .none => .none, }, .repeat = self.config.bg_image_repeat, }, }; // Signal that the buffer was modified. self.bg_image_buffer_modified +%= 1; }
/// Update custom shader uniforms that depend on terminal state. /// /// This should be called in `updateFrame` when terminal state changes. fn updateCustomShaderUniformsFromState(self: *Self) void { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return;
// Only update when terminal state is dirty. if (self.terminal_state.dirty == .false) return;
const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms; const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors;
// 256-color palette for (colors.palette, 0..) |color, i| { uniforms.palette[i] = .{ @as(f32, @floatFromInt(color.r)) / 255.0, @as(f32, @floatFromInt(color.g)) / 255.0, @as(f32, @floatFromInt(color.b)) / 255.0, 1.0, }; }
// Background color uniforms.background_color = .{ @as(f32, @floatFromInt(colors.background.r)) / 255.0, @as(f32, @floatFromInt(colors.background.g)) / 255.0, @as(f32, @floatFromInt(colors.background.b)) / 255.0, 1.0, };
// Foreground color uniforms.foreground_color = .{ @as(f32, @floatFromInt(colors.foreground.r)) / 255.0, @as(f32, @floatFromInt(colors.foreground.g)) / 255.0, @as(f32, @floatFromInt(colors.foreground.b)) / 255.0, 1.0, };
// Cursor color if (colors.cursor) |cursor_color| { uniforms.cursor_color = .{ @as(f32, @floatFromInt(cursor_color.r)) / 255.0, @as(f32, @floatFromInt(cursor_color.g)) / 255.0, @as(f32, @floatFromInt(cursor_color.b)) / 255.0, 1.0, }; }
// NOTE: the following could be optimized to follow a change in // config for a slight optimization however this is only 12 bytes // each being updated and likely isn't a cause for concern
// Cursor text color if (self.config.cursor_text) |cursor_text| { uniforms.cursor_text = .{ @as(f32, @floatFromInt(cursor_text.color.r)) / 255.0, @as(f32, @floatFromInt(cursor_text.color.g)) / 255.0, @as(f32, @floatFromInt(cursor_text.color.b)) / 255.0, 1.0, }; }
// Selection background color if (self.config.selection_background) |selection_bg| { uniforms.selection_background_color = .{ @as(f32, @floatFromInt(selection_bg.color.r)) / 255.0, @as(f32, @floatFromInt(selection_bg.color.g)) / 255.0, @as(f32, @floatFromInt(selection_bg.color.b)) / 255.0, 1.0, }; }
// Selection foreground color if (self.config.selection_foreground) |selection_fg| { uniforms.selection_foreground_color = .{ @as(f32, @floatFromInt(selection_fg.color.r)) / 255.0, @as(f32, @floatFromInt(selection_fg.color.g)) / 255.0, @as(f32, @floatFromInt(selection_fg.color.b)) / 255.0, 1.0, }; }
// Cursor visibility uniforms.cursor_visible = @intFromBool(self.terminal_state.cursor.visible);
// Cursor style const cursor_style: renderer.CursorStyle = .fromTerminal(self.terminal_state.cursor.visual_style); uniforms.previous_cursor_style = uniforms.current_cursor_style; uniforms.current_cursor_style = @as(i32, @intFromEnum(cursor_style)); }
/// Update per-frame custom shader uniforms. /// /// This should be called exactly once per frame, inside `drawFrame`. fn updateCustomShaderUniformsForFrame(self: *Self) !void { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return;
const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
const now = try std.time.Instant.now(); defer self.last_frame_time = now; const first_frame_time = self.first_frame_time orelse t: { self.first_frame_time = now; break :t now; }; const last_frame_time = self.last_frame_time orelse now;
const since_ns: f32 = @floatFromInt(now.since(first_frame_time)); uniforms.time = since_ns / std.time.ns_per_s;
const delta_ns: f32 = @floatFromInt(now.since(last_frame_time)); uniforms.time_delta = delta_ns / std.time.ns_per_s;
uniforms.frame += 1;
const screen = self.size.screen; const padding = self.size.padding; const cell = self.size.cell;
uniforms.resolution = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, }; uniforms.channel_resolution[0] = .{ @floatFromInt(screen.width), @floatFromInt(screen.height), 1, 0, };
if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
// Left edge of the cell the cursor is in. var pixel_x: f32 = @floatFromInt( cursor.grid_pos[0] * cell.width + padding.left, ); // Top edge, relative to the top of the // screen, of the cell the cursor is in. var pixel_y: f32 = @floatFromInt( cursor.grid_pos[1] * cell.height + padding.top, );
// If +Y is up in our shaders, we need to flip the coordinate // so that it's instead the top edge of the cell relative to // the *bottom* of the screen. if (!GraphicsAPI.custom_shader_y_is_down) { pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y; }
// Add the X bearing to get the -X (left) edge of the cursor. pixel_x += @floatFromInt(cursor.bearings[0]);
// How we deal with the Y bearing depends on which direction // is "up", since we want our final `pixel_y` value to be the // +Y edge of the cursor. if (GraphicsAPI.custom_shader_y_is_down) { // As a reminder, the Y bearing is the distance from the // bottom of the cell to the top of the glyph, so to get // the +Y edge we need to add the cell height, subtract // the Y bearing, and add the glyph height to get the +Y // (bottom) edge of the cursor. pixel_y += @floatFromInt(cell.height); pixel_y -= @floatFromInt(cursor.bearings[1]); pixel_y += @floatFromInt(cursor.glyph_size[1]); } else { // If the Y direction is reversed though, we instead want // the *top* edge of the cursor, which means we just need // to subtract the cell height and add the Y bearing. pixel_y -= @floatFromInt(cell.height); pixel_y += @floatFromInt(cursor.bearings[1]); }
const new_cursor: [4]f32 = .{ pixel_x, pixel_y, cursor_width, cursor_height, }; const cursor_color: [4]f32 = .{ @as(f32, @floatFromInt(cursor.color[0])) / 255.0, @as(f32, @floatFromInt(cursor.color[1])) / 255.0, @as(f32, @floatFromInt(cursor.color[2])) / 255.0, @as(f32, @floatFromInt(cursor.color[3])) / 255.0, };
const cursor_changed: bool = !std.meta.eql(new_cursor, uniforms.current_cursor) or !std.meta.eql(cursor_color, uniforms.current_cursor_color);
if (cursor_changed) { uniforms.previous_cursor = uniforms.current_cursor; uniforms.previous_cursor_color = uniforms.current_cursor_color; uniforms.current_cursor = new_cursor; uniforms.current_cursor_color = cursor_color; uniforms.cursor_change_time = uniforms.time; } }
// Update focus uniforms uniforms.focus = @intFromBool(self.focused);
// If we need to update the time our focus state changed // then update it to our current frame time. This may not be // exactly correct since it is frame time, not exact focus // time, but focus time on its own isn't exactly correct anyways // since it comes async from a message. if (self.custom_shader_focused_changed and self.focused) { uniforms.time_focus = uniforms.time; self.custom_shader_focused_changed = false; } }
/// Build the overlay as configured. Returns null if there is no /// overlay currently configured. fn rebuildOverlay( self: *Self, features: []const Overlay.Feature, ) Overlay.InitError!void { // const start = std.time.Instant.now() catch unreachable; // const start_micro = std.time.microTimestamp(); // defer { // const end = std.time.Instant.now() catch unreachable; // log.warn( // "[rebuildOverlay time] start_micro={} duration={}ns", // .{ start_micro, end.since(start) / std.time.ns_per_us }, // ); // }
const alloc = self.alloc;
// If we have no features enabled, don't build an overlay. // If we had a previous overlay, deallocate it. if (features.len == 0) { if (self.overlay) |*old| { old.deinit(alloc); self.overlay = null; }
return; }
// If we had a previous overlay, clear it. Otherwise, init. const overlay: *Overlay = overlay: { if (self.overlay) |*v| existing: { // Verify that our overlay size matches our screen // size as we know it now. If not, deinit and reinit. // Note: these intCasts are always safe because z2d // stores as i32 but we always init with a u32. const width: u32 = @intCast(v.surface.getWidth()); const height: u32 = @intCast(v.surface.getHeight()); const term_size = self.size.terminal(); if (width != term_size.width or height != term_size.height) break :existing;
// We also depend on cell size. if (v.cell_size.width != self.size.cell.width or v.cell_size.height != self.size.cell.height) break :existing;
// Everything matches, so we can just reset the surface // and redraw. v.reset(); break :overlay v; }
// If we reached this point we want to reset our overlay. if (self.overlay) |*v| { v.deinit(alloc); self.overlay = null; }
assert(self.overlay == null); const new: Overlay = try .init(alloc, self.size); self.overlay = new; break :overlay &self.overlay.?; }; overlay.applyFeatures( alloc, &self.terminal_state, features, ); }
const PreeditRange = struct { y: terminal.size.CellCountInt, x: [2]terminal.size.CellCountInt, cp_offset: usize, };
/// Convert the terminal state to GPU cells stored in CPU memory. These /// are then synced to the GPU in the next frame. This only updates CPU /// memory and doesn't touch the GPU. /// /// This requires the draw mutex. /// /// Dirty state on terminal state won't be reset by this. fn rebuildCells( self: *Self, preedit: ?renderer.State.Preedit, cursor_style_: ?renderer.CursorStyle, links: *const terminal.RenderState.CellSet, ) Allocator.Error!void { const state: *terminal.RenderState = &self.terminal_state;
// const start = try std.time.Instant.now(); // const start_micro = std.time.microTimestamp(); // defer { // const end = std.time.Instant.now() catch unreachable; // // "[rebuildCells time] <START us>\t<TIME_TAKEN us>" // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); // }
const grid_size_diff = self.cells.size.rows != state.rows or self.cells.size.columns != state.cols;
if (grid_size_diff) { var new_size = self.cells.size; new_size.rows = state.rows; new_size.columns = state.cols; try self.cells.resize(self.alloc, new_size);
// Update our uniforms accordingly, otherwise // our background cells will be out of place. self.uniforms.grid_size = .{ new_size.columns, new_size.rows }; }
const rebuild = state.dirty == .full or grid_size_diff; if (rebuild) { // If we are doing a full rebuild, then we clear the entire cell buffer. self.cells.reset();
// We also reset our padding extension depending on the screen type switch (self.config.padding_color) { .background => {},
// For extension, assume we are extending in all directions. // For "extend" this may be disabled due to heuristics below. .extend, .@"extend-always" => { self.uniforms.padding_extend = .{ .up = true, .down = true, .left = true, .right = true, }; }, } }
// From this point on we never fail. We produce some kind of // working terminal state, even if incorrect. errdefer comptime unreachable;
// Get our row data from our state const row_data = state.row_data.slice(); const row_raws = row_data.items(.raw); const row_cells = row_data.items(.cells); const row_dirty = row_data.items(.dirty); const row_selection = row_data.items(.selection); const row_highlights = row_data.items(.highlights);
// If our cell contents buffer is shorter than the screen viewport, // we render the rows that fit, starting from the bottom. If instead // the viewport is shorter than the cell contents buffer, we align // the top of the viewport with the top of the contents buffer. const row_len: usize = @min( state.rows, self.cells.size.rows, );
// Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. const preedit_range: ?PreeditRange = if (preedit) |preedit_v| preedit: { // We base the preedit on the position of the cursor in the // viewport. If the cursor isn't visible in the viewport we // don't show it. const cursor_vp = state.cursor.viewport orelse break :preedit null;
// If our preedit row isn't dirty then we don't need the // preedit range. This also avoids an issue later where we // unconditionally add preedit cells when this is set. if (!rebuild and !row_dirty[cursor_vp.y]) break :preedit null;
const range = preedit_v.range( cursor_vp.x, state.cols - 1, ); break :preedit .{ .y = @intCast(cursor_vp.y), .x = .{ range.start, range.end }, .cp_offset = range.cp_offset, }; } else null;
for ( 0.., row_raws[0..row_len], row_cells[0..row_len], row_dirty[0..row_len], row_selection[0..row_len], row_highlights[0..row_len], ) |y_usize, row, *cells, *dirty, selection, *highlights| { const y: terminal.size.CellCountInt = @intCast(y_usize);
if (!rebuild) { // Only rebuild if we are doing a full rebuild or this row is dirty. if (!dirty.*) continue;
// Clear the cells if the row is dirty self.cells.clear(y); }
// Unmark the dirty state in our render state. dirty.* = false;
self.rebuildRow( y, row, cells, preedit_range, selection, highlights, links, ) catch |err| { // This should never happen except under exceptional // scenarios. In this case, we don't want to corrupt // our render state so just clear this row and keep // trying to finish it out. log.warn("error building row y={} err={}", .{ y, err }); self.cells.clear(y); }; }
// Setup our cursor rendering information. cursor: { // Clear our cursor by default. self.cells.setCursor(null, null); self.uniforms.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16), };
// If the cursor isn't visible on the viewport, don't show // a cursor. Otherwise, get our cursor cell, because we may // need it for styling. const cursor_vp = state.cursor.viewport orelse break :cursor; const cursor_style: terminal.Style = cursor_style: { const cells = state.row_data.items(.cells); const cell = cells[cursor_vp.y].get(cursor_vp.x); break :cursor_style if (cell.raw.hasStyling()) cell.style else .{}; };
// If we have preedit text, we don't setup a cursor if (preedit != null) break :cursor;
// If there isn't a cursor visual style requested then // we don't render a cursor. const style = cursor_style_ orelse break :cursor;
// Determine the cursor color. const cursor_color = cursor_color: { // If an explicit cursor color was set by OSC 12, use that. if (state.colors.cursor) |v| break :cursor_color v;
// Use our configured color if specified if (self.config.cursor_color) |v| switch (v) { .color => |color| break :cursor_color color.toTerminalRGB(),
inline .@"cell-foreground", .@"cell-background", => |_, tag| { const fg_style = cursor_style.fg(.{ .default = state.colors.foreground, .palette = &state.colors.palette, .bold = self.config.bold_color, }); const bg_style = cursor_style.bg( &state.cursor.cell, &state.colors.palette, ) orelse state.colors.background;
break :cursor_color switch (tag) { .color => unreachable, .@"cell-foreground" => if (cursor_style.flags.inverse) bg_style else fg_style, .@"cell-background" => if (cursor_style.flags.inverse) fg_style else bg_style, }; }, };
break :cursor_color state.colors.foreground; };
self.addCursor( &state.cursor, style, cursor_color, );
// If the cursor is visible then we set our uniforms. if (style == .block) { const wide = state.cursor.cell.wide;
self.uniforms.cursor_pos = .{ // If we are a spacer tail of a wide cell, our cursor needs // to move back one cell. The saturate is to ensure we don't // overflow but this shouldn't happen with well-formed input. switch (wide) { .narrow, .spacer_head, .wide => cursor_vp.x, .spacer_tail => cursor_vp.x -| 1, }, @intCast(cursor_vp.y), };
self.uniforms.bools.cursor_wide = switch (wide) { .narrow, .spacer_head => false, .wide, .spacer_tail => true, };
const uniform_color = if (self.config.cursor_text) |txt| blk: { // If cursor-text is set, then compute the correct color. // Otherwise, use the background color. if (txt == .color) { // Use the color set by cursor-text, if any. break :blk txt.color.toTerminalRGB(); }
const fg_style = cursor_style.fg(.{ .default = state.colors.foreground, .palette = &state.colors.palette, .bold = self.config.bold_color, }); const bg_style = cursor_style.bg( &state.cursor.cell, &state.colors.palette, ) orelse state.colors.background;
break :blk switch (txt) { // If the cell is reversed, use the opposite cell color instead. .@"cell-foreground" => if (cursor_style.flags.inverse) bg_style else fg_style, .@"cell-background" => if (cursor_style.flags.inverse) fg_style else bg_style, else => unreachable, }; } else state.colors.background;
self.uniforms.cursor_color = .{ uniform_color.r, uniform_color.g, uniform_color.b, 255, }; } }
// Setup our preedit text. if (preedit) |preedit_v| preedit: { const range = preedit_range orelse break :preedit; var x = range.x[0]; for (preedit_v.codepoints[range.cp_offset..]) |cp| { self.addPreeditCell( cp, .{ .x = x, .y = range.y }, state.colors.foreground, ) catch |err| { log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ x, range.y, err, }); };
x += if (cp.wide) 2 else 1; } }
// Update that our cells rebuilt self.cells_rebuilt = true;
// Log some things // log.debug("rebuildCells complete cached_runs={}", .{ // self.font_shaper_cache.count(), // }); }
fn rebuildRow( self: *Self, y: terminal.size.CellCountInt, row: terminal.page.Row, cells: *std.MultiArrayList(terminal.RenderState.Cell), preedit_range: ?PreeditRange, selection: ?[2]terminal.size.CellCountInt, highlights: *const std.ArrayList(terminal.RenderState.Highlight), links: *const terminal.RenderState.CellSet, ) !void { const state = &self.terminal_state;
// If our viewport is wider than our cell contents buffer, // we still only process cells up to the width of the buffer. const cells_slice = cells.slice(); const cells_len = @min(cells_slice.len, self.cells.size.columns); const cells_raw = cells_slice.items(.raw); const cells_style = cells_slice.items(.style);
// On primary screen, we still apply vertical padding // extension under certain conditions we feel are safe. // // This helps make some scenarios look better while // avoiding scenarios we know do NOT look good. switch (self.config.padding_color) { // These already have the correct values set above. .background, .@"extend-always" => {},
// Apply heuristics for padding extension. .extend => if (y == 0) { self.uniforms.padding_extend.up = !rowNeverExtendBg( row, cells_raw, cells_style, &state.colors.palette, state.colors.background, ); } else if (y == self.cells.size.rows - 1) { self.uniforms.padding_extend.down = !rowNeverExtendBg( row, cells_raw, cells_style, &state.colors.palette, state.colors.background, ); }, }
// Iterator of runs for shaping. var run_iter_opts: font.shape.RunOptions = .{ .grid = self.font_grid, .cells = cells_slice, .selection = if (selection) |s| s else null,
// We want to do font shaping as long as the cursor is // visible on this viewport. .cursor_x = cursor_x: { const vp = state.cursor.viewport orelse break :cursor_x null; if (vp.y != y) break :cursor_x null; break :cursor_x vp.x; }, }; run_iter_opts.applyBreakConfig(self.config.font_shaping_break); var run_iter = self.font_shaper.runIterator(run_iter_opts); var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); var shaper_cells: ?[]const font.shape.Cell = null; var shaper_cells_i: usize = 0;
for ( 0.., cells_raw[0..cells_len], cells_style[0..cells_len], ) |x, *cell, *managed_style| { // If this cell falls within our preedit range then we // skip this because preedits are setup separately. if (preedit_range) |range| preedit: { // We're not on the preedit line, no actions necessary. if (range.y != y) break :preedit; // We're before the preedit range, no actions necessary. if (x < range.x[0]) break :preedit; // We're in the preedit range, skip this cell. if (x <= range.x[1]) continue; // After exiting the preedit range we need to catch // the run position up because of the missed cells. // In all other cases, no action is necessary. if (x != range.x[1] + 1) break :preedit;
// Step the run iterator until we find a run that ends // after the current cell, which will be the soonest run // that might contain glyphs for our cell. while (shaper_run) |run| { if (run.offset + run.cells > x) break; shaper_run = try run_iter.next(self.alloc); shaper_cells = null; shaper_cells_i = 0; }
const run = shaper_run orelse break :preedit;
// If we haven't shaped this run, do so now. shaper_cells = shaper_cells orelse // Try to read the cells from the shaping cache if we can. self.font_shaper_cache.get(run) orelse cache: { // Otherwise we have to shape them. const new_cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we // continue because it is just a performance optimization, // not a correctness issue. self.font_shaper_cache.put( self.alloc, run, new_cells, ) catch |err| { log.warn( "error caching font shaping results err={}", .{err}, ); };
// The cells we get from direct shaping are always owned // by the shaper and valid until the next shaping call so // we can safely use them. break :cache new_cells; };
// Advance our index until we reach or pass // our current x position in the shaper cells. const shaper_cells_unwrapped = shaper_cells.?; while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) { shaper_cells_i += 1; } }
const wide = cell.wide; const style: terminal.Style = if (cell.hasStyling()) managed_style.* else .{};
// True if this cell is selected const selected: enum { false, selection, search, search_selected, } = selected: { // Order below matters for precedence.
// Selection should take the highest precedence. const x_compare = if (wide == .spacer_tail) x -| 1 else x; if (selection) |sel| { if (x_compare >= sel[0] and x_compare <= sel[1]) break :selected .selection; }
// If we're highlighted, then we're selected. In the // future we want to use a different style for this // but this to get started. for (highlights.items) |hl| { if (x_compare >= hl.range[0] and x_compare <= hl.range[1]) { const tag: HighlightTag = @enumFromInt(hl.tag); break :selected switch (tag) { .search_match => .search, .search_match_selected => .search_selected, }; } }
break :selected .false; };
// The `_style` suffixed values are the colors based on // the cell style (SGR), before applying any additional // configuration, inversions, selections, etc. const bg_style = style.bg( cell, &state.colors.palette, ); const fg_style = style.fg(.{ .default = state.colors.foreground, .palette = &state.colors.palette, .bold = self.config.bold_color, });
// The final background color for the cell. const bg = switch (selected) { // If we have an explicit selection background color // specified in the config, use that. // // If no configuration, then our selection background // is our foreground color. .selection => if (self.config.selection_background) |v| switch (v) { .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, } else state.colors.foreground,
.search => switch (self.config.search_background) { .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, },
.search_selected => switch (self.config.search_selected_background) { .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, },
// Not selected .false => if (style.flags.inverse != isCovering(cell.codepoint())) // Two cases cause us to invert (use the fg color as the bg) // - The "inverse" style flag. // - A "covering" glyph; we use fg for bg in that // case to help make sure that padding extension // works correctly. // // If one of these is true (but not the other) // then we use the fg style color for the bg. fg_style else // Otherwise they cancel out. bg_style, };
const fg = fg: { // Our happy-path non-selection background color // is our style or our configured defaults. const final_bg = bg_style orelse state.colors.background;
// Whether we need to use the bg color as our fg color: // - Cell is selected, inverted, and set to cell-foreground // - Cell is selected, not inverted, and set to cell-background // - Cell is inverted and not selected break :fg switch (selected) { .selection => if (self.config.selection_foreground) |v| switch (v) { .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, } else state.colors.background,
.search => switch (self.config.search_foreground) { .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, },
.search_selected => switch (self.config.search_selected_foreground) { .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, },
.false => if (style.flags.inverse) final_bg else fg_style, }; };
// Foreground alpha for this cell. const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255;
// Set the cell's background color. { const rgb = bg orelse state.colors.background;
// Determine our background alpha. If we have transparency configured // then this is dynamic depending on some situations. This is all // in an attempt to make transparency look the best for various // situations. See inline comments. const bg_alpha: u8 = bg_alpha: { const default: u8 = 255;
// Cells that are selected should be fully opaque. if (selected != .false) break :bg_alpha default;
// Cells that are reversed should be fully opaque. if (style.flags.inverse) break :bg_alpha default;
// If the user requested to have opacity on all cells, apply it. if (self.config.background_opacity_cells and bg_style != null) { var opacity: f64 = @floatFromInt(default); opacity *= self.config.background_opacity; break :bg_alpha @intFromFloat(opacity); }
// Cells that have an explicit bg color should be fully opaque. if (bg_style != null) break :bg_alpha default;
// Otherwise, we won't draw the bg for this cell, // we'll let the already-drawn background color // show through. break :bg_alpha 0; };
self.cells.bgCell(y, x).* = .{ rgb.r, rgb.g, rgb.b, bg_alpha, }; }
// If the invisible flag is set on this cell then we // don't need to render any foreground elements, so // we just skip all glyphs with this x coordinate. // // NOTE: This behavior matches xterm. Some other terminal // emulators, e.g. Alacritty, still render text decorations // and only make the text itself invisible. The decision // has been made here to match xterm's behavior for this. if (style.flags.invisible) { continue; }
// Give links a single underline, unless they already have // an underline, in which case use a double underline to // distinguish them. const underline: terminal.Attribute.Underline = underline: { if (links.contains(.{ .x = @intCast(x), .y = @intCast(y), })) { break :underline if (style.flags.underline == .single) .double else .single; } break :underline style.flags.underline; };
// We draw underlines first so that they layer underneath text. // This improves readability when a colored underline is used // which intersects parts of the text (descenders). if (underline != .none) self.addUnderline( @intCast(x), @intCast(y), underline, style.underlineColor(&state.colors.palette) orelse fg, alpha, ) catch |err| { log.warn( "error adding underline to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); };
if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { log.warn( "error adding overline to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); };
// If we're at or past the end of our shaper run then // we need to get the next run from the run iterator. if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { shaper_run = try run_iter.next(self.alloc); shaper_cells = null; shaper_cells_i = 0; }
if (shaper_run) |run| glyphs: { // If we haven't shaped this run yet, do so. shaper_cells = shaper_cells orelse // Try to read the cells from the shaping cache if we can. self.font_shaper_cache.get(run) orelse cache: { // Otherwise we have to shape them. const new_cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we // continue because it is just a performance optimization, // not a correctness issue. self.font_shaper_cache.put( self.alloc, run, new_cells, ) catch |err| { log.warn( "error caching font shaping results err={}", .{err}, ); };
// The cells we get from direct shaping are always owned // by the shaper and valid until the next shaping call so // we can safely use them. break :cache new_cells; };
const shaped_cells = shaper_cells orelse break :glyphs;
// If there are no shaper cells for this run, ignore it. // This can occur for runs of empty cells, and is fine. if (shaped_cells.len == 0) break :glyphs;
// If we encounter a shaper cell to the left of the current // cell then we have some problems. This logic relies on x // position monotonically increasing. assert(run.offset + shaped_cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never // be present in more than one shaper run. If that assumption is // violated, this logic breaks.
while (shaper_cells_i < shaped_cells.len and run.offset + shaped_cells[shaper_cells_i].x == x) : ({ shaper_cells_i += 1; }) { self.addGlyph( @intCast(x), @intCast(y), state.cols, cells_raw, shaped_cells[shaper_cells_i], shaper_run.?, fg, alpha, ) catch |err| { log.warn( "error adding glyph to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); }; } }
// Finally, draw a strikethrough if necessary. if (style.flags.strikethrough) self.addStrikethrough( @intCast(x), @intCast(y), fg, alpha, ) catch |err| { log.warn( "error adding strikethrough to cell, will be invalid x={} y={}, err={}", .{ x, y, err }, ); }; } }
/// Add an underline decoration to the specified cell fn addUnderline( self: *Self, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, style: terminal.Attribute.Underline, color: terminal.color.RGB, alpha: u8, ) !void { const sprite: font.Sprite = switch (style) { .none => unreachable, .single => .underline, .double => .underline_double, .dotted => .underline_dotted, .dashed => .underline_dashed, .curly => .underline_curly, };
const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), .{ .cell_width = 1, .grid_metrics = self.grid_metrics, }, );
try self.cells.add(self.alloc, .underline, .{ .atlas = .grayscale, .grid_pos = .{ @intCast(x), @intCast(y) }, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @intCast(render.glyph.offset_x), @intCast(render.glyph.offset_y), }, }); }
/// Add a overline decoration to the specified cell fn addOverline( self: *Self, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, color: terminal.color.RGB, alpha: u8, ) !void { const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(font.Sprite.overline), .{ .cell_width = 1, .grid_metrics = self.grid_metrics, }, );
try self.cells.add(self.alloc, .overline, .{ .atlas = .grayscale, .grid_pos = .{ @intCast(x), @intCast(y) }, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @intCast(render.glyph.offset_x), @intCast(render.glyph.offset_y), }, }); }
/// Add a strikethrough decoration to the specified cell fn addStrikethrough( self: *Self, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, color: terminal.color.RGB, alpha: u8, ) !void { const render = try self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(font.Sprite.strikethrough), .{ .cell_width = 1, .grid_metrics = self.grid_metrics, }, );
try self.cells.add(self.alloc, .strikethrough, .{ .atlas = .grayscale, .grid_pos = .{ @intCast(x), @intCast(y) }, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @intCast(render.glyph.offset_x), @intCast(render.glyph.offset_y), }, }); }
// Add a glyph to the specified cell. fn addGlyph( self: *Self, x: terminal.size.CellCountInt, y: terminal.size.CellCountInt, cols: usize, cell_raws: []const terminal.page.Cell, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, color: terminal.color.RGB, alpha: u8, ) !void { const cell = cell_raws[x]; const cp = cell.codepoint();
// Render const render = try self.font_grid.renderGlyph( self.alloc, shaper_run.font_index, shaper_cell.glyph_index, .{ .grid_metrics = self.grid_metrics, .thicken = self.config.font_thicken, .thicken_strength = self.config.font_thicken_strength, .cell_width = cell.gridWidth(), // If there's no Nerd Font constraint for this codepoint // then, if it's a symbol, we constrain it to fit inside // its cell(s), we don't modify the alignment at all. .constraint = getConstraint(cp) orelse if (cellpkg.isSymbol(cp)) .{ .size = .fit, } else .none, .constraint_width = constraintWidth( cell_raws, x, cols, ), }, );
// If the glyph is 0 width or height, it will be invisible // when drawn, so don't bother adding it to the buffer. if (render.glyph.width == 0 or render.glyph.height == 0) { return; }
try self.cells.add(self.alloc, .text, .{ .atlas = switch (render.presentation) { .emoji => .color, .text => .grayscale, }, .bools = .{ .no_min_contrast = noMinContrast(cp) }, .grid_pos = .{ @intCast(x), @intCast(y) }, .color = .{ color.r, color.g, color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @intCast(render.glyph.offset_x + shaper_cell.x_offset), @intCast(render.glyph.offset_y + shaper_cell.y_offset), }, }); }
fn addCursor( self: *Self, cursor_state: *const terminal.RenderState.Cursor, cursor_style: renderer.CursorStyle, cursor_color: terminal.color.RGB, ) void { const cursor_vp = cursor_state.viewport orelse return;
// Add the cursor. We render the cursor over the wide character if // we're on the wide character tail. const wide, const x = cell: { // The cursor goes over the screen cursor position. if (!cursor_vp.wide_tail) break :cell .{ cursor_state.cell.wide == .wide, cursor_vp.x, };
// If we're part of a wide character, we move the cursor back // to the actual character. break :cell .{ true, cursor_vp.x - 1 }; };
const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha = 255 * self.config.cursor_opacity; break :alpha @intFromFloat(@ceil(alpha)); };
const render = switch (cursor_style) { .block, .block_hollow, .bar, .underline, => render: { const sprite: font.Sprite = switch (cursor_style) { .block => .cursor_rect, .block_hollow => .cursor_hollow_rect, .bar => .cursor_bar, .underline => .cursor_underline, .lock => unreachable, };
break :render self.font_grid.renderGlyph( self.alloc, font.sprite_index, @intFromEnum(sprite), .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return; }; },
.lock => self.font_grid.renderCodepoint( self.alloc, 0xF023, // lock symbol .regular, .text, .{ .cell_width = if (wide) 2 else 1, .grid_metrics = self.grid_metrics, }, ) catch |err| { log.warn("error rendering cursor glyph err={}", .{err}); return; } orelse { // This should never happen because we embed nerd // fonts so we just log and return instead of fallback. log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); return; }, };
self.cells.setCursor(.{ .atlas = .grayscale, .bools = .{ .is_cursor_glyph = true }, .grid_pos = .{ x, cursor_vp.y }, .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @intCast(render.glyph.offset_x), @intCast(render.glyph.offset_y), }, }, cursor_style); }
fn addPreeditCell( self: *Self, cp: renderer.State.Preedit.Codepoint, coord: terminal.Coordinate, screen_fg: terminal.color.RGB, ) !void { // Render the glyph for our preedit text const render_ = self.font_grid.renderCodepoint( self.alloc, @intCast(cp.codepoint), .regular, .text, .{ .grid_metrics = self.grid_metrics }, ) catch |err| { log.warn("error rendering preedit glyph err={}", .{err}); return; }; const render = render_ orelse { log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); return; };
// Add our text try self.cells.add(self.alloc, .text, .{ .atlas = .grayscale, .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, .color = .{ screen_fg.r, screen_fg.g, screen_fg.b, 255 }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_size = .{ render.glyph.width, render.glyph.height }, .bearings = .{ @intCast(render.glyph.offset_x), @intCast(render.glyph.offset_y), }, });
// Add underline try self.addUnderline(@intCast(coord.x), @intCast(coord.y), .single, screen_fg, 255); if (cp.wide and coord.x < self.cells.size.columns - 1) { try self.addUnderline(@intCast(coord.x + 1), @intCast(coord.y), .single, screen_fg, 255); } }
/// Sync the atlas data to the given texture. This copies the bytes /// associated with the atlas to the given texture. If the atlas no /// longer fits into the texture, the texture will be resized. fn syncAtlasTexture( self: *const Self, atlas: *const font.Atlas, texture: *Texture, ) !void { if (atlas.size > texture.width) { // Free our old texture texture.*.deinit();
// Reallocate texture.* = try self.api.initAtlasTexture(atlas); }
try texture.replaceRegion(0, 0, atlas.size, atlas.size, atlas.data); } };}