-
Notifications
You must be signed in to change notification settings - Fork 793
/
Copy pathOpenGL.zig
2700 lines (2371 loc) · 93.2 KB
/
OpenGL.zig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! Rendering implementation for OpenGL.
pub const OpenGL = @This();
const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const assert = std.debug.assert;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const link = @import("link.zig");
const isCovering = @import("cell.zig").isCovering;
const fgMode = @import("cell.zig").fgMode;
const shadertoy = @import("shadertoy.zig");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");
const imgui = @import("imgui");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const Terminal = terminal.Terminal;
const gl = @import("opengl");
const math = @import("../math.zig");
const Surface = @import("../Surface.zig");
const CellProgram = @import("opengl/CellProgram.zig");
const ImageProgram = @import("opengl/ImageProgram.zig");
const gl_image = @import("opengl/image.zig");
const custom = @import("opengl/custom.zig");
const Image = gl_image.Image;
const ImageMap = gl_image.ImageMap;
const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement);
const log = std.log.scoped(.grid);
/// The runtime can request a single-threaded draw by setting this boolean
/// to true. In this case, the renderer.draw() call is expected to be called
/// from the runtime.
pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw"))
apprt.Surface.opengl_single_threaded_draw
else
false;
const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void;
const drawMutexZero: DrawMutex = if (DrawMutex == void) void{} else .{};
alloc: std.mem.Allocator,
/// The configuration we need derived from the main config.
config: DerivedConfig,
/// Current font metrics defining our grid.
grid_metrics: font.Metrics,
/// The size of everything.
size: renderer.Size,
/// The current set of cells to render. Each set of cells goes into
/// a separate shader call.
cells_bg: std.ArrayListUnmanaged(CellProgram.Cell),
cells: std.ArrayListUnmanaged(CellProgram.Cell),
/// The last viewport that we based our rebuild off of. If this changes,
/// then we do a full rebuild of the cells. The pointer values in this pin
/// are NOT SAFE to read because they may be modified, freed, etc from the
/// termio thread. We treat the pointers as integers for comparison only.
cells_viewport: ?terminal.Pin = null,
/// The size of the cells list that was sent to the GPU. This is used
/// to detect when the cells array was reallocated/resized and handle that
/// accordingly.
gl_cells_size: usize = 0,
/// The last length of the cells that was written to the GPU. This is used to
/// determine what data needs to be rewritten on the GPU.
gl_cells_written: usize = 0,
/// Shader program for cell rendering.
gl_state: ?GLState = null,
/// The font structures.
font_grid: *font.SharedGrid,
font_shaper: font.Shaper,
font_shaper_cache: font.ShaperCache,
texture_grayscale_modified: usize = 0,
texture_grayscale_resized: usize = 0,
texture_color_modified: usize = 0,
texture_color_resized: usize = 0,
/// True if the window is focused
focused: bool,
/// The foreground color set by an OSC 10 sequence. If unset then the default
/// value from the config file is used.
foreground_color: ?terminal.color.RGB,
/// Foreground color set in the user's config file.
default_foreground_color: terminal.color.RGB,
/// The background color set by an OSC 11 sequence. If unset then the default
/// value from the config file is used.
background_color: ?terminal.color.RGB,
/// Background color set in the user's config file.
default_background_color: terminal.color.RGB,
/// The cursor color set by an OSC 12 sequence. If unset then
/// default_cursor_color is used.
cursor_color: ?terminal.color.RGB,
/// Default cursor color when no color is set explicitly by an OSC 12 command.
/// This is cursor color as set in the user's config, if any. If no cursor color
/// is set in the user's config, then the cursor color is determined by the
/// current foreground color.
default_cursor_color: ?terminal.color.RGB,
/// When `cursor_color` is null, swap the foreground and background colors of
/// the cell under the cursor for the cursor color. Otherwise, use the default
/// foreground color as the cursor color.
cursor_invert: bool,
/// The mailbox for communicating with the window.
surface_mailbox: apprt.surface.Mailbox,
/// Deferred operations. This is used to apply changes to the OpenGL context.
/// Some runtimes (GTK) do not support multi-threading so to keep our logic
/// simple we apply all OpenGL context changes in the render() call.
deferred_screen_size: ?SetScreenSize = null,
deferred_font_size: ?SetFontSize = null,
deferred_config: ?SetConfig = null,
/// If we're drawing with single threaded operations
draw_mutex: DrawMutex = drawMutexZero,
/// Current background to draw. This may not match self.background if the
/// terminal is in reversed mode.
draw_background: terminal.color.RGB,
/// Whether we're doing padding extension for vertical sides.
padding_extend_top: bool = true,
padding_extend_bottom: bool = true,
/// The images that we may render.
images: ImageMap = .{},
image_placements: ImagePlacementList = .{},
image_bg_end: u32 = 0,
image_text_end: u32 = 0,
image_virtual: bool = false,
/// Deferred OpenGL operation to update the screen size.
const SetScreenSize = struct {
size: renderer.Size,
fn apply(self: SetScreenSize, r: *OpenGL) !void {
const gl_state: *GLState = if (r.gl_state) |*v|
v
else
return error.OpenGLUninitialized;
// Apply our padding
const grid_size = self.size.grid();
const terminal_size = self.size.terminal();
// Blank space around the grid.
const blank: renderer.Padding = switch (r.config.padding_color) {
// We can use zero padding because the background color is our
// clear color.
.background => .{},
.extend, .@"extend-always" => self.size.screen.blankPadding(
self.size.padding,
grid_size,
self.size.cell,
).add(self.size.padding),
};
// Update our viewport for this context to be the entire window.
// OpenGL works in pixels, so we have to use the pixel size.
try gl.viewport(
0,
0,
@intCast(self.size.screen.width),
@intCast(self.size.screen.height),
);
// Update the projection uniform within our shader
inline for (.{ "cell_program", "image_program" }) |name| {
const program = @field(gl_state, name);
const bind = try program.program.use();
defer bind.unbind();
try program.program.setUniform(
"projection",
// 2D orthographic projection with the full w/h
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)),
),
);
}
// Setup our grid padding
{
const program = gl_state.cell_program;
const bind = try program.program.use();
defer bind.unbind();
try program.program.setUniform(
"grid_padding",
@Vector(4, f32){
@floatFromInt(blank.top),
@floatFromInt(blank.right),
@floatFromInt(blank.bottom),
@floatFromInt(blank.left),
},
);
try program.program.setUniform(
"grid_size",
@Vector(2, f32){
@floatFromInt(grid_size.columns),
@floatFromInt(grid_size.rows),
},
);
}
// Update our custom shader resolution
if (gl_state.custom) |*custom_state| {
try custom_state.setScreenSize(self.size);
}
}
};
const SetFontSize = struct {
metrics: font.Metrics,
fn apply(self: SetFontSize, r: *const OpenGL) !void {
const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
inline for (.{ "cell_program", "image_program" }) |name| {
const program = @field(gl_state, name);
const bind = try program.program.use();
defer bind.unbind();
try program.program.setUniform(
"cell_size",
@Vector(2, f32){
@floatFromInt(self.metrics.cell_width),
@floatFromInt(self.metrics.cell_height),
},
);
}
}
};
const SetConfig = struct {
fn apply(self: SetConfig, r: *const OpenGL) !void {
_ = self;
const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
const bind = try gl_state.cell_program.program.use();
defer bind.unbind();
try gl_state.cell_program.program.setUniform(
"min_contrast",
r.config.min_contrast,
);
}
};
/// 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,
cursor_color: ?terminal.color.RGB,
cursor_invert: bool,
cursor_text: ?terminal.color.RGB,
cursor_opacity: f64,
background: terminal.color.RGB,
background_opacity: f64,
foreground: terminal.color.RGB,
selection_background: ?terminal.color.RGB,
selection_foreground: ?terminal.color.RGB,
invert_selection_fg_bg: bool,
bold_is_bright: bool,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
links: link.Set,
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 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,
);
const cursor_invert = config.@"cursor-invert-fg-bg";
return .{
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
.font_thicken = config.@"font-thicken",
.font_thicken_strength = config.@"font-thicken-strength",
.font_features = font_features.list,
.font_styles = font_styles,
.cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
config.@"cursor-color".?.toTerminalRGB()
else
null,
.cursor_invert = cursor_invert,
.cursor_text = if (config.@"cursor-text") |txt|
txt.toTerminalRGB()
else
null,
.cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.invert_selection_fg_bg = config.@"selection-invert-fg-bg",
.bold_is_bright = config.@"bold-is-bright",
.min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color",
.selection_background = if (config.@"selection-background") |bg|
bg.toTerminalRGB()
else
null,
.selection_foreground = if (config.@"selection-foreground") |bg|
bg.toTerminalRGB()
else
null,
.custom_shaders = custom_shaders,
.links = links,
.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) !OpenGL {
// Create the initial font shaper
var shaper = try font.Shaper.init(alloc, .{
.features = options.config.font_features.items,
});
errdefer shaper.deinit();
// For the remainder of the setup we lock our font grid data because
// we're reading it.
const grid = options.font_grid;
grid.lock.lockShared();
defer grid.lock.unlockShared();
var gl_state = try GLState.init(alloc, options.config, grid);
errdefer gl_state.deinit();
return OpenGL{
.alloc = alloc,
.config = options.config,
.cells_bg = .{},
.cells = .{},
.grid_metrics = grid.metrics,
.size = options.size,
.gl_state = gl_state,
.font_grid = grid,
.font_shaper = shaper,
.font_shaper_cache = font.ShaperCache.init(),
.draw_background = options.config.background,
.focused = true,
.foreground_color = null,
.default_foreground_color = options.config.foreground,
.background_color = null,
.default_background_color = options.config.background,
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.cursor_invert = options.config.cursor_invert,
.surface_mailbox = options.surface_mailbox,
.deferred_font_size = .{ .metrics = grid.metrics },
.deferred_config = .{},
};
}
pub fn deinit(self: *OpenGL) void {
self.font_shaper.deinit();
self.font_shaper_cache.deinit(self.alloc);
{
var it = self.images.iterator();
while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc);
self.images.deinit(self.alloc);
}
self.image_placements.deinit(self.alloc);
if (self.gl_state) |*v| v.deinit(self.alloc);
self.cells.deinit(self.alloc);
self.cells_bg.deinit(self.alloc);
self.config.deinit();
self.* = undefined;
}
/// Returns the hints that we want for this
pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints {
return .{
.context_version_major = 3,
.context_version_minor = 3,
.opengl_profile = .opengl_core_profile,
.opengl_forward_compat = true,
.cocoa_graphics_switching = builtin.os.tag == .macos,
.cocoa_retina_framebuffer = true,
.transparent_framebuffer = config.@"background-opacity" < 1,
};
}
/// This is called early right after surface creation.
pub fn surfaceInit(surface: *apprt.Surface) !void {
// Treat this like a thread entry
const self: OpenGL = undefined;
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => {
// GTK uses global OpenGL context so we load from null.
const version = try gl.glad.load(null);
const major = gl.glad.versionMajor(@intCast(version));
const minor = gl.glad.versionMinor(@intCast(version));
errdefer gl.glad.unload();
log.info("loaded OpenGL {}.{}", .{ major, minor });
// We require at least OpenGL 3.3
if (major < 3 or (major == 3 and minor < 3)) {
log.warn("OpenGL version is too old. Ghostty requires OpenGL 3.3", .{});
return error.OpenGLOutdated;
}
},
apprt.glfw => try self.threadEnter(surface),
apprt.embedded => {
// TODO(mitchellh): this does nothing today to allow libghostty
// to compile for OpenGL targets but libghostty is strictly
// broken for rendering on this platforms.
},
}
// These are very noisy so this is commented, but easy to uncomment
// whenever we need to check the OpenGL extension list
// if (builtin.mode == .Debug) {
// var ext_iter = try gl.ext.iterator();
// while (try ext_iter.next()) |ext| {
// log.debug("OpenGL extension available name={s}", .{ext});
// }
// }
}
/// This is called just prior to spinning up the renderer thread for
/// final main thread setup requirements.
pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
_ = surface;
// For GLFW, we grabbed the OpenGL context in surfaceInit and we
// need to release it before we start the renderer thread.
if (apprt.runtime == apprt.glfw) {
glfw.makeContextCurrent(null);
}
}
/// Called when the OpenGL context is made invalid, so we need to free
/// all previous resources and stop rendering.
pub fn displayUnrealized(self: *OpenGL) void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
if (self.gl_state) |*v| {
v.deinit(self.alloc);
self.gl_state = null;
}
}
/// Called when the OpenGL is ready to be initialized.
pub fn displayRealize(self: *OpenGL) !void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Make our new state
var gl_state = gl_state: {
self.font_grid.lock.lockShared();
defer self.font_grid.lock.unlockShared();
break :gl_state try GLState.init(
self.alloc,
self.config,
self.font_grid,
);
};
errdefer gl_state.deinit();
// Unrealize if we have to
if (self.gl_state) |*v| v.deinit(self.alloc);
// Set our new state
self.gl_state = gl_state;
// Make sure we invalidate all the fields so that we
// reflush everything
self.gl_cells_size = 0;
self.gl_cells_written = 0;
self.texture_grayscale_modified = 0;
self.texture_color_modified = 0;
self.texture_grayscale_resized = 0;
self.texture_color_resized = 0;
// We need to reset our uniforms
self.deferred_screen_size = .{ .size = self.size };
self.deferred_font_size = .{ .metrics = self.grid_metrics };
self.deferred_config = .{};
}
/// Callback called by renderer.Thread when it begins.
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => {
// GTK doesn't support threaded OpenGL operations as far as I can
// tell, so we use the renderer thread to setup all the state
// but then do the actual draws and texture syncs and all that
// on the main thread. As such, we don't do anything here.
},
apprt.glfw => {
// We need to make the OpenGL context current. OpenGL requires
// that a single thread own the a single OpenGL context (if any). This
// ensures that the context switches over to our thread. Important:
// the prior thread MUST have detached the context prior to calling
// this entrypoint.
glfw.makeContextCurrent(surface.window);
errdefer glfw.makeContextCurrent(null);
glfw.swapInterval(1);
// Load OpenGL bindings. This API is context-aware so this sets
// a threadlocal context for these pointers.
const version = try gl.glad.load(&glfw.getProcAddress);
errdefer gl.glad.unload();
log.info("loaded OpenGL {}.{}", .{
gl.glad.versionMajor(@intCast(version)),
gl.glad.versionMinor(@intCast(version)),
});
},
apprt.embedded => {
// TODO(mitchellh): this does nothing today to allow libghostty
// to compile for OpenGL targets but libghostty is strictly
// broken for rendering on this platforms.
},
}
}
/// Callback called by renderer.Thread when it exits.
pub fn threadExit(self: *const OpenGL) void {
_ = self;
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
apprt.gtk => {
// We don't need to do any unloading for GTK because we may
// be sharing the global bindings with other windows.
},
apprt.glfw => {
gl.glad.unload();
glfw.makeContextCurrent(null);
},
apprt.embedded => {
// TODO: see threadEnter
},
}
}
/// True if our renderer has animations so that a higher frequency
/// timer is used.
pub fn hasAnimations(self: *const OpenGL) bool {
const state = self.gl_state orelse return false;
return state.custom != null;
}
/// See Metal
pub fn hasVsync(self: *const OpenGL) bool {
_ = self;
// OpenGL currently never has vsync
return false;
}
/// See Metal.
pub fn markDirty(self: *OpenGL) void {
// Do nothing, we don't have dirty tracking yet.
_ = self;
}
/// Callback when the focus changes for the terminal this is rendering.
///
/// Must be called on the render thread.
pub fn setFocus(self: *OpenGL, focus: bool) !void {
self.focused = focus;
}
/// Callback when the window is visible or occluded.
///
/// Must be called on the render thread.
pub fn setVisible(self: *OpenGL, visible: bool) void {
_ = self;
_ = visible;
}
/// Set the new font grid.
///
/// Must be called on the render thread.
pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void {
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Reset our font grid
self.font_grid = grid;
self.grid_metrics = grid.metrics;
self.texture_grayscale_modified = 0;
self.texture_grayscale_resized = 0;
self.texture_color_modified = 0;
self.texture_color_resized = 0;
// 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 our screen size because the font grid can affect grid
// metrics which update uniforms.
self.deferred_screen_size = .{ .size = self.size };
// Defer our GPU updates
self.deferred_font_size = .{ .metrics = grid.metrics };
}
/// The primary render callback that is completely thread-safe.
pub fn updateFrame(
self: *OpenGL,
surface: *apprt.Surface,
state: *renderer.State,
cursor_blink_visible: bool,
) !void {
_ = surface;
// Data we extract out of the critical area.
const Critical = struct {
full_rebuild: bool,
gl_bg: terminal.color.RGB,
screen: terminal.Screen,
screen_type: terminal.ScreenType,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit,
cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette,
};
// Update all our data as tightly as possible within the mutex.
var critical: Critical = critical: {
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;
}
// Swap bg/fg if the terminal is reversed
const bg = self.background_color orelse self.default_background_color;
const fg = self.foreground_color orelse self.default_foreground_color;
defer {
if (self.background_color) |*c| {
c.* = bg;
} else {
self.default_background_color = bg;
}
if (self.foreground_color) |*c| {
c.* = fg;
} else {
self.default_foreground_color = fg;
}
}
if (state.terminal.modes.get(.reverse_colors)) {
if (self.background_color) |*c| {
c.* = fg;
} else {
self.default_background_color = fg;
}
if (self.foreground_color) |*c| {
c.* = bg;
} else {
self.default_foreground_color = bg;
}
}
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock while rebuilding GPU cells.
var screen_copy = try state.terminal.screen.clone(
self.alloc,
.{ .viewport = .{} },
null,
);
errdefer screen_copy.deinit();
// Whether to draw our cursor or not.
const cursor_style = if (state.terminal.flags.password_input)
.lock
else
renderer.cursorStyle(
state,
self.focused,
cursor_blink_visible,
);
// Get our preedit state
const preedit: ?renderer.State.Preedit = preedit: {
if (cursor_style == null) break :preedit null;
const p = state.preedit orelse break :preedit null;
break :preedit try p.clone(self.alloc);
};
errdefer if (preedit) |p| p.deinit(self.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 (state.terminal.screen.kitty_images.dirty or
self.image_virtual)
{
// prepKittyGraphics touches self.images which is also used
// in drawFrame so if we're drawing on a separate thread we need
// to lock this.
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
try self.prepKittyGraphics(state.terminal);
}
// If we have any terminal dirty flags set then we need to rebuild
// the entire screen. This can be optimized in the future.
const full_rebuild: bool = rebuild: {
{
const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?;
const v: Int = @bitCast(state.terminal.flags.dirty);
if (v > 0) break :rebuild true;
}
{
const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?;
const v: Int = @bitCast(state.terminal.screen.dirty);
if (v > 0) break :rebuild true;
}
// If our viewport changed then we need to rebuild the entire
// screen because it means we scrolled. If we have no previous
// viewport then we must rebuild.
const prev_viewport = self.cells_viewport orelse break :rebuild true;
if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
break :rebuild false;
};
// Reset the dirty flags in the terminal and screen. We assume
// that our rebuild will be successful since so we optimize for
// success and reset while we hold the lock. This is much easier
// than coordinating row by row or as changes are persisted.
state.terminal.flags.dirty = .{};
state.terminal.screen.dirty = .{};
{
var it = state.terminal.screen.pages.pageIterator(
.right_down,
.{ .screen = .{} },
null,
);
while (it.next()) |chunk| {
var dirty_set = chunk.node.data.dirtyBitSet();
dirty_set.unsetAll();
}
}
// Update our viewport pin for dirty tracking
self.cells_viewport = viewport_pin;
break :critical .{
.full_rebuild = full_rebuild,
.gl_bg = self.background_color orelse self.default_background_color,
.screen = screen_copy,
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
.preedit = preedit,
.cursor_style = cursor_style,
.color_palette = state.terminal.color_palette.colors,
};
};
defer {
critical.screen.deinit();
if (critical.preedit) |p| p.deinit(self.alloc);
}
// Grab our draw mutex if we have it and update our data
{
if (single_threaded_draw) self.draw_mutex.lock();
defer if (single_threaded_draw) self.draw_mutex.unlock();
// Set our draw data
self.draw_background = critical.gl_bg;
// Build our GPU cells
try self.rebuildCells(
critical.full_rebuild,
&critical.screen,
critical.screen_type,
critical.mouse,
critical.preedit,
critical.cursor_style,
&critical.color_palette,
);
// Notify our shaper we're done for the frame. For some shapers like
// CoreText this triggers off-thread cleanup logic.
self.font_shaper.endFrame();
}
}
/// This goes through the Kitty graphic placements and accumulates the
/// placements we need to render on our viewport. It also ensures that
/// the visible images are loaded on the GPU.
fn prepKittyGraphics(
self: *OpenGL,
t: *terminal.Terminal,
) !void {
const storage = &t.screen.kitty_images;
defer storage.dirty = false;
// We always clear our previous placements no matter what because
// we rebuild them from scratch.
self.image_placements.clearRetainingCapacity();
self.image_virtual = false;
// Go through our known images and if there are any that are no longer
// in use then mark them to be freed.
//
// This never conflicts with the below because a placement can't
// reference an image that doesn't exist.
{
var it = self.images.iterator();
while (it.next()) |kv| {
if (storage.imageById(kv.key_ptr.*) == null) {
kv.value_ptr.image.markForUnload();
}
}
}
// The top-left and bottom-right corners of our viewport in screen
// points. This lets us determine offsets and containment of placements.
const top = t.screen.pages.getTopLeft(.viewport);
const bot = t.screen.pages.getBottomRight(.viewport).?;
// Go through the placements and ensure the image is loaded on the GPU.
var it = storage.placements.iterator();
while (it.next()) |kv| {
// Find the image in storage
const p = kv.value_ptr;
// Special logic based on location
switch (p.location) {
.pin => {},
.virtual => {
// We need to mark virtual placements on our renderer so that
// we know to rebuild in more scenarios since cell changes can
// now trigger placement changes.
self.image_virtual = true;
// We also continue out because virtual placements are
// only triggered by the unicode placeholder, not by the
// placement itself.
continue;
},
}
const image = storage.imageById(kv.key_ptr.image_id) orelse {
log.warn(
"missing image for placement, ignoring image_id={}",
.{kv.key_ptr.image_id},
);
continue;
};
try self.prepKittyPlacement(t, &top, &bot, &image, p);
}
// If we have virtual placements then we need to scan for placeholders.
if (self.image_virtual) {
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
t,
&virtual_p,
);
}
// Sort the placements by their Z value.
std.mem.sortUnstable(
gl_image.Placement,
self.image_placements.items,
{},
struct {
fn lessThan(
ctx: void,
lhs: gl_image.Placement,
rhs: gl_image.Placement,
) bool {
_ = ctx;
return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id);
}
}.lessThan,
);
// Find our indices. The values are sorted by z so we can find the
// first placement out of bounds to find the limits.
var bg_end: ?u32 = null;
var text_end: ?u32 = null;
const bg_limit = std.math.minInt(i32) / 2;
for (self.image_placements.items, 0..) |p, i| {
if (bg_end == null and p.z >= bg_limit) {
bg_end = @intCast(i);
}
if (text_end == null and p.z >= 0) {
text_end = @intCast(i);
}
}
self.image_bg_end = bg_end orelse 0;
self.image_text_end = text_end orelse self.image_bg_end;
}
fn prepKittyVirtualPlacement(
self: *OpenGL,
t: *terminal.Terminal,
p: *const terminal.kitty.graphics.unicode.Placement,
) !void {
const storage = &t.screen.kitty_images;
const image = storage.imageById(p.image_id) orelse {