mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
Miscellaneous Renderer Cleanup (#7671)
This PR is a collection of small improvements, cleanup, and fixes to the renderer, as a follow-up to #7620. ### Summary of changes - The nearly identical-between-backends `cell.zig` and `image.zig` are now both unified. - Shader pipeline preparation is now even DRYer in preparation for future changes that will involve adding new pipelines, where I don't want to be slowed down by having to add them in multiple places to get things working. There is now a single source of truth at the top of `shaders.zig` for what core pipelines are available and how they're configured. - Global background color drawn in a separate step before individual cell background colors, this is required to be able to draw kitty images between these layers. Can't use the clear color for this because that would require color space conversions on the CPU-side which we don't yet have utilities for. - Fixed a bug with the kitty image z-index logic where not having foreground images made the background images act as foreground images. - Moved the custom shader uniform buffer to the frame state so we don't create a new one every frame like we were before. - Fixed color glyphs under OpenGL being channel-swapped because the texture format was RGBA when the data was BGRA (#7670).
This commit is contained in:
@@ -3,6 +3,7 @@ const std = @import("std");
|
||||
pub const png = @import("png.zig");
|
||||
pub const jpeg = @import("jpeg.zig");
|
||||
pub const swizzle = @import("swizzle.zig");
|
||||
pub const Error = @import("error.zig").Error;
|
||||
|
||||
pub const ImageData = struct {
|
||||
width: u32,
|
||||
|
@@ -75,7 +75,7 @@ fn initTarget(
|
||||
self.metallib = .create(b, .{
|
||||
.name = "Ghostty",
|
||||
.target = target,
|
||||
.sources = &.{b.path("src/renderer/shaders/cell.metal")},
|
||||
.sources = &.{b.path("src/renderer/shaders/shaders.metal")},
|
||||
});
|
||||
|
||||
// Change our config
|
||||
|
44
src/datastruct/array_list_collection.zig
Normal file
44
src/datastruct/array_list_collection.zig
Normal file
@@ -0,0 +1,44 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// A collection of ArrayLists with methods for bulk operations.
|
||||
pub fn ArrayListCollection(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = ArrayListCollection(T);
|
||||
const ArrayListT = std.ArrayListUnmanaged(T);
|
||||
|
||||
// An array containing the lists that belong to this collection.
|
||||
lists: []ArrayListT,
|
||||
|
||||
// The collection will be initialized with empty ArrayLists.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
list_count: usize,
|
||||
initial_capacity: usize,
|
||||
) Allocator.Error!Self {
|
||||
const self: Self = .{
|
||||
.lists = try alloc.alloc(ArrayListT, list_count),
|
||||
};
|
||||
|
||||
for (self.lists) |*list| {
|
||||
list.* = try .initCapacity(alloc, initial_capacity);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, alloc: Allocator) void {
|
||||
for (self.lists) |*list| {
|
||||
list.deinit(alloc);
|
||||
}
|
||||
alloc.free(self.lists);
|
||||
}
|
||||
|
||||
/// Clear all lists in the collection, retaining capacity.
|
||||
pub fn reset(self: *Self) void {
|
||||
for (self.lists) |*list| {
|
||||
list.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@@ -50,15 +50,18 @@ modified: std.atomic.Value(usize) = .{ .raw = 0 },
|
||||
resized: std.atomic.Value(usize) = .{ .raw = 0 },
|
||||
|
||||
pub const Format = enum(u8) {
|
||||
/// 1 byte per pixel grayscale.
|
||||
grayscale = 0,
|
||||
rgb = 1,
|
||||
rgba = 2,
|
||||
/// 3 bytes per pixel BGR.
|
||||
bgr = 1,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
bgra = 2,
|
||||
|
||||
pub fn depth(self: Format) u8 {
|
||||
return switch (self) {
|
||||
.grayscale => 1,
|
||||
.rgb => 3,
|
||||
.rgba => 4,
|
||||
.bgr => 3,
|
||||
.bgra => 4,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -303,7 +306,12 @@ pub fn clear(self: *Atlas) void {
|
||||
}
|
||||
|
||||
/// Dump the atlas as a PPM to a writer, for debug purposes.
|
||||
/// Only supports grayscale and rgb atlases.
|
||||
/// Only supports grayscale and bgr atlases.
|
||||
///
|
||||
/// NOTE: BGR atlases will have the red and blue channels
|
||||
/// swapped because PPM expects RGB. This would be
|
||||
/// easy enough to fix so next time someone needs
|
||||
/// to debug a color atlas they should fix it.
|
||||
pub fn dump(self: Atlas, writer: anytype) !void {
|
||||
try writer.print(
|
||||
\\P{c}
|
||||
@@ -313,7 +321,7 @@ pub fn dump(self: Atlas, writer: anytype) !void {
|
||||
, .{
|
||||
@as(u8, switch (self.format) {
|
||||
.grayscale => '5',
|
||||
.rgb => '6',
|
||||
.bgr => '6',
|
||||
else => {
|
||||
log.err("Unsupported format for dump: {}", .{self.format});
|
||||
@panic("Cannot dump this atlas format.");
|
||||
@@ -418,8 +426,16 @@ pub const Wasm = struct {
|
||||
|
||||
// We need to draw pixels so this is format dependent.
|
||||
const buf: []u8 = switch (self.format) {
|
||||
// RGBA is the native ImageData format
|
||||
.rgba => self.data,
|
||||
.bgra => buf: {
|
||||
// Convert from BGRA to RGBA by swapping every R and B.
|
||||
var buf: []u8 = try alloc.dupe(u8, self.data);
|
||||
errdefer alloc.free(buf);
|
||||
var i: usize = 0;
|
||||
while (i < self.data.len) : (i += 4) {
|
||||
std.mem.swap(u8, &buf[i], &buf[i + 2]);
|
||||
}
|
||||
break :buf buf;
|
||||
},
|
||||
|
||||
.grayscale => buf: {
|
||||
// Convert from A8 to RGBA so every 4th byte is set to a value.
|
||||
@@ -572,12 +588,12 @@ test "grow" {
|
||||
try testing.expectEqual(@as(u8, 4), atlas.data[atlas.size * 2 + 2]);
|
||||
}
|
||||
|
||||
test "writing RGB data" {
|
||||
test "writing BGR data" {
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try init(alloc, 32, .rgb);
|
||||
var atlas = try init(alloc, 32, .bgr);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
// This is RGB so its 3 bpp
|
||||
// This is BGR so its 3 bpp
|
||||
const reg = try atlas.reserve(alloc, 1, 2);
|
||||
atlas.set(reg, &[_]u8{
|
||||
1, 2, 3,
|
||||
@@ -594,18 +610,18 @@ test "writing RGB data" {
|
||||
try testing.expectEqual(@as(u8, 6), atlas.data[65 * depth + 2]);
|
||||
}
|
||||
|
||||
test "grow RGB" {
|
||||
test "grow BGR" {
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Atlas is 4x4 so its a 1px border meaning we only have 2x2 available
|
||||
var atlas = try init(alloc, 4, .rgb);
|
||||
var atlas = try init(alloc, 4, .bgr);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
// Get our 2x2, which should be ALL our usable space
|
||||
const reg = try atlas.reserve(alloc, 2, 2);
|
||||
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
|
||||
|
||||
// This is RGB so its 3 bpp
|
||||
// This is BGR so its 3 bpp
|
||||
atlas.set(reg, &[_]u8{
|
||||
10, 11, 12, // (0, 0) (x, y) from top-left
|
||||
13, 14, 15, // (1, 0)
|
||||
|
@@ -79,7 +79,7 @@ pub fn init(
|
||||
|
||||
var atlas_grayscale = try Atlas.init(alloc, 512, .grayscale);
|
||||
errdefer atlas_grayscale.deinit(alloc);
|
||||
var atlas_color = try Atlas.init(alloc, 512, .rgba);
|
||||
var atlas_color = try Atlas.init(alloc, 512, .bgra);
|
||||
errdefer atlas_color.deinit(alloc);
|
||||
|
||||
var result: SharedGrid = .{
|
||||
|
@@ -391,7 +391,7 @@ pub const Face = struct {
|
||||
const format: ?font.Atlas.Format = switch (bitmap_ft.pixel_mode) {
|
||||
freetype.c.FT_PIXEL_MODE_MONO => null,
|
||||
freetype.c.FT_PIXEL_MODE_GRAY => .grayscale,
|
||||
freetype.c.FT_PIXEL_MODE_BGRA => .rgba,
|
||||
freetype.c.FT_PIXEL_MODE_BGRA => .bgra,
|
||||
else => {
|
||||
log.warn("glyph={} pixel mode={}", .{ glyph_index, bitmap_ft.pixel_mode });
|
||||
@panic("unsupported pixel mode");
|
||||
@@ -925,7 +925,7 @@ test "color emoji" {
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .rgba);
|
||||
var atlas = try font.Atlas.init(alloc, 512, .bgra);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(
|
||||
@@ -973,14 +973,14 @@ test "color emoji" {
|
||||
}
|
||||
}
|
||||
|
||||
test "mono to rgba" {
|
||||
test "mono to bgra" {
|
||||
const alloc = testing.allocator;
|
||||
const testFont = font.embedded.emoji;
|
||||
|
||||
var lib = try Library.init(alloc);
|
||||
defer lib.deinit();
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .rgba);
|
||||
var atlas = try font.Atlas.init(alloc, 512, .bgra);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } });
|
||||
|
@@ -29,9 +29,6 @@ pub const Buffer = bufferpkg.Buffer;
|
||||
pub const Texture = @import("metal/Texture.zig");
|
||||
pub const shaders = @import("metal/shaders.zig");
|
||||
|
||||
pub const cellpkg = @import("metal/cell.zig");
|
||||
pub const imagepkg = @import("metal/image.zig");
|
||||
|
||||
pub const custom_shader_target: shadertoy.Target = .msl;
|
||||
// The fragCoord for Metal shaders is +Y = down.
|
||||
pub const custom_shader_y_is_down = true;
|
||||
@@ -305,6 +302,44 @@ pub inline fn textureOptions(self: Metal) Texture.Options {
|
||||
};
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
pub const ImageTextureFormat = enum {
|
||||
/// 1 byte per pixel grayscale.
|
||||
gray,
|
||||
/// 4 bytes per pixel RGBA.
|
||||
rgba,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
bgra,
|
||||
|
||||
fn toPixelFormat(
|
||||
self: ImageTextureFormat,
|
||||
srgb: bool,
|
||||
) mtl.MTLPixelFormat {
|
||||
return switch (self) {
|
||||
.gray => if (srgb) .r8unorm_srgb else .r8unorm,
|
||||
.rgba => if (srgb) .rgba8unorm_srgb else .rgba8unorm,
|
||||
.bgra => if (srgb) .bgra8unorm_srgb else .bgra8unorm,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the options to use when constructing textures for images.
|
||||
pub inline fn imageTextureOptions(
|
||||
self: Metal,
|
||||
format: ImageTextureFormat,
|
||||
srgb: bool,
|
||||
) Texture.Options {
|
||||
return .{
|
||||
.device = self.device,
|
||||
.pixel_format = format.toPixelFormat(srgb),
|
||||
.resource_options = .{
|
||||
// Indicate that the CPU writes to this resource but never reads it.
|
||||
.cpu_cache_mode = .write_combined,
|
||||
.storage_mode = self.default_storage_mode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Initializes a Texture suitable for the provided font atlas.
|
||||
pub fn initAtlasTexture(
|
||||
self: *const Metal,
|
||||
@@ -312,7 +347,7 @@ pub fn initAtlasTexture(
|
||||
) Texture.Error!Texture {
|
||||
const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) {
|
||||
.grayscale => .r8unorm,
|
||||
.rgba => .bgra8unorm,
|
||||
.bgra => .bgra8unorm_srgb,
|
||||
else => @panic("unsupported atlas format for Metal texture"),
|
||||
};
|
||||
|
||||
|
@@ -24,9 +24,6 @@ pub const Buffer = bufferpkg.Buffer;
|
||||
pub const Texture = @import("opengl/Texture.zig");
|
||||
pub const shaders = @import("opengl/shaders.zig");
|
||||
|
||||
pub const cellpkg = @import("opengl/cell.zig");
|
||||
pub const imagepkg = @import("opengl/image.zig");
|
||||
|
||||
pub const custom_shader_target: shadertoy.Target = .glsl;
|
||||
// The fragCoord for OpenGL shaders is +Y = up.
|
||||
pub const custom_shader_y_is_down = false;
|
||||
@@ -402,6 +399,38 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
|
||||
};
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
pub const ImageTextureFormat = enum {
|
||||
/// 1 byte per pixel grayscale.
|
||||
gray,
|
||||
/// 4 bytes per pixel RGBA.
|
||||
rgba,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
bgra,
|
||||
|
||||
fn toPixelFormat(self: ImageTextureFormat) gl.Texture.Format {
|
||||
return switch (self) {
|
||||
.gray => .red,
|
||||
.rgba => .rgba,
|
||||
.bgra => .bgra,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the options to use when constructing textures for images.
|
||||
pub inline fn imageTextureOptions(
|
||||
self: OpenGL,
|
||||
format: ImageTextureFormat,
|
||||
srgb: bool,
|
||||
) Texture.Options {
|
||||
_ = self;
|
||||
return .{
|
||||
.format = format.toPixelFormat(),
|
||||
.internal_format = if (srgb) .srgba else .rgba,
|
||||
.target = .Rectangle,
|
||||
};
|
||||
}
|
||||
|
||||
/// Initializes a Texture suitable for the provided font atlas.
|
||||
pub fn initAtlasTexture(
|
||||
self: *const OpenGL,
|
||||
@@ -411,7 +440,7 @@ pub fn initAtlasTexture(
|
||||
const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat =
|
||||
switch (atlas.format) {
|
||||
.grayscale => .{ .red, .red },
|
||||
.rgba => .{ .rgba, .srgba },
|
||||
.bgra => .{ .bgra, .srgba },
|
||||
else => @panic("unsupported atlas format for OpenGL texture"),
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,197 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const font = @import("../font/main.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const shaderpkg = renderer.Renderer.API.shaders;
|
||||
const ArrayListCollection = @import("../datastruct/array_list_collection.zig").ArrayListCollection;
|
||||
|
||||
/// The possible cell content keys that exist.
|
||||
pub const Key = enum {
|
||||
bg,
|
||||
text,
|
||||
underline,
|
||||
strikethrough,
|
||||
overline,
|
||||
|
||||
/// Returns the GPU vertex type for this key.
|
||||
pub fn CellType(self: Key) type {
|
||||
return switch (self) {
|
||||
.bg => shaderpkg.CellBg,
|
||||
|
||||
.text,
|
||||
.underline,
|
||||
.strikethrough,
|
||||
.overline,
|
||||
=> shaderpkg.CellText,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// The contents of all the cells in the terminal.
|
||||
///
|
||||
/// The goal of this data structure is to allow for efficient row-wise
|
||||
/// clearing of data from the GPU buffers, to allow for row-wise dirty
|
||||
/// tracking to eliminate the overhead of rebuilding the GPU buffers
|
||||
/// each frame.
|
||||
///
|
||||
/// Must be initialized by resizing before calling any operations.
|
||||
pub const Contents = struct {
|
||||
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
|
||||
|
||||
/// Flat array containing cell background colors for the terminal grid.
|
||||
///
|
||||
/// Indexed as `bg_cells[row * size.columns + col]`.
|
||||
///
|
||||
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
|
||||
/// of directly indexing in order to avoid integer size bugs.
|
||||
bg_cells: []shaderpkg.CellBg = undefined,
|
||||
|
||||
/// The ArrayListCollection which holds all of the foreground cells. When
|
||||
/// sized with Contents.resize the individual ArrayLists are given enough
|
||||
/// room that they can hold a single row with #cols glyphs, underlines, and
|
||||
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
|
||||
/// it is possible to exceed this with combining glyphs that add a glyph
|
||||
/// but take up no column since they combine with the previous one, as
|
||||
/// well as with fonts that perform multi-substitutions for glyphs, which
|
||||
/// can result in a similar situation where multiple glyphs reside in the
|
||||
/// same column.
|
||||
///
|
||||
/// Allocations should nevertheless be exceedingly rare since hitting the
|
||||
/// initial capacity of a list would require a row filled with underlined
|
||||
/// struck through characters, at least one of which is a multi-glyph
|
||||
/// composite.
|
||||
///
|
||||
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
|
||||
/// the collection is reserved for the cursor, which must be the first item
|
||||
/// in the buffer.
|
||||
///
|
||||
/// Must be initialized by calling resize on the Contents struct before
|
||||
/// calling any operations.
|
||||
fg_rows: ArrayListCollection(shaderpkg.CellText) = .{ .lists = &.{} },
|
||||
|
||||
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
||||
alloc.free(self.bg_cells);
|
||||
self.fg_rows.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Resize the cell contents for the given grid size. This will
|
||||
/// always invalidate the entire cell contents.
|
||||
pub fn resize(
|
||||
self: *Contents,
|
||||
alloc: Allocator,
|
||||
size: renderer.GridSize,
|
||||
) Allocator.Error!void {
|
||||
self.size = size;
|
||||
|
||||
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
|
||||
|
||||
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
|
||||
errdefer alloc.free(bg_cells);
|
||||
|
||||
@memset(bg_cells, .{ 0, 0, 0, 0 });
|
||||
|
||||
// The foreground lists can hold 3 types of items:
|
||||
// - Glyphs
|
||||
// - Underlines
|
||||
// - Strikethroughs
|
||||
// So we give them an initial capacity of size.columns * 3, which will
|
||||
// avoid any further allocations in the vast majority of cases. Sadly
|
||||
// we can not assume capacity though, since with combining glyphs that
|
||||
// form a single grapheme, and multi-substitutions in fonts, the number
|
||||
// of glyphs in a row is theoretically unlimited.
|
||||
//
|
||||
// We have size.rows + 1 lists because index 0 is used for a special
|
||||
// list containing the cursor cell which needs to be first in the buffer.
|
||||
var fg_rows = try ArrayListCollection(shaderpkg.CellText).init(
|
||||
alloc,
|
||||
size.rows + 1,
|
||||
size.columns * 3,
|
||||
);
|
||||
errdefer fg_rows.deinit(alloc);
|
||||
|
||||
alloc.free(self.bg_cells);
|
||||
self.fg_rows.deinit(alloc);
|
||||
|
||||
self.bg_cells = bg_cells;
|
||||
self.fg_rows = fg_rows;
|
||||
|
||||
// We don't need 3*cols worth of cells for the cursor list, so we can
|
||||
// replace it with a smaller list. This is technically a tiny bit of
|
||||
// extra work but resize is not a hot function so it's worth it to not
|
||||
// waste the memory.
|
||||
self.fg_rows.lists[0].deinit(alloc);
|
||||
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(
|
||||
shaderpkg.CellText,
|
||||
).initCapacity(alloc, 1);
|
||||
}
|
||||
|
||||
/// Reset the cell contents to an empty state without resizing.
|
||||
pub fn reset(self: *Contents) void {
|
||||
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
|
||||
self.fg_rows.reset();
|
||||
}
|
||||
|
||||
/// Set the cursor value. If the value is null then the cursor is hidden.
|
||||
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
|
||||
self.fg_rows.lists[0].clearRetainingCapacity();
|
||||
|
||||
if (v) |cell| {
|
||||
self.fg_rows.lists[0].appendAssumeCapacity(cell);
|
||||
}
|
||||
}
|
||||
|
||||
/// Access a background cell. Prefer this function over direct indexing
|
||||
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
|
||||
pub inline fn bgCell(
|
||||
self: *Contents,
|
||||
row: usize,
|
||||
col: usize,
|
||||
) *shaderpkg.CellBg {
|
||||
return &self.bg_cells[row * self.size.columns + col];
|
||||
}
|
||||
|
||||
/// Add a cell to the appropriate list. Adding the same cell twice will
|
||||
/// result in duplication in the vertex buffer. The caller should clear
|
||||
/// the corresponding row with Contents.clear to remove old cells first.
|
||||
pub fn add(
|
||||
self: *Contents,
|
||||
alloc: Allocator,
|
||||
comptime key: Key,
|
||||
cell: key.CellType(),
|
||||
) Allocator.Error!void {
|
||||
const y = cell.grid_pos[1];
|
||||
|
||||
assert(y < self.size.rows);
|
||||
|
||||
switch (key) {
|
||||
.bg => comptime unreachable,
|
||||
|
||||
.text,
|
||||
.underline,
|
||||
.strikethrough,
|
||||
.overline,
|
||||
// We have a special list containing the cursor cell at the start
|
||||
// of our fg row collection, so we need to add 1 to the y to get
|
||||
// the correct index.
|
||||
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all of the cell contents for a given row.
|
||||
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
||||
assert(y < self.size.rows);
|
||||
|
||||
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
|
||||
|
||||
// We have a special list containing the cursor cell at the start
|
||||
// of our fg row collection, so we need to add 1 to the y to get
|
||||
// the correct index.
|
||||
self.fg_rows.lists[y + 1].clearRetainingCapacity();
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns true if a codepoint for a cell is a covering character. A covering
|
||||
/// character is a character that covers the entire cell. This is used to
|
||||
@@ -38,7 +229,7 @@ pub const FgMode = enum {
|
||||
pub fn fgMode(
|
||||
presentation: font.Presentation,
|
||||
cell_pin: terminal.Pin,
|
||||
) !FgMode {
|
||||
) FgMode {
|
||||
return switch (presentation) {
|
||||
// Emoji is always full size and color.
|
||||
.emoji => .color,
|
||||
@@ -131,3 +322,141 @@ fn isPowerline(char: u21) bool {
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
test Contents {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
|
||||
var c: Contents = .{};
|
||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||
defer c.deinit(alloc);
|
||||
|
||||
// We should start off empty after resizing.
|
||||
for (0..rows) |y| {
|
||||
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
|
||||
for (0..cols) |x| {
|
||||
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
|
||||
}
|
||||
}
|
||||
// And the cursor row should have a capacity of 1 and also be empty.
|
||||
try testing.expect(c.fg_rows.lists[0].capacity == 1);
|
||||
try testing.expect(c.fg_rows.lists[0].items.len == 0);
|
||||
|
||||
// Add some contents.
|
||||
const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell: shaderpkg.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 1 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(1, 4).* = bg_cell;
|
||||
try c.add(alloc, .text, fg_cell);
|
||||
try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
|
||||
// The fg row index is offset by 1 because of the cursor list.
|
||||
try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
|
||||
|
||||
// And we should be able to clear it.
|
||||
c.clear(1);
|
||||
for (0..rows) |y| {
|
||||
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
|
||||
for (0..cols) |x| {
|
||||
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a cursor.
|
||||
const cursor_cell: shaderpkg.CellText = .{
|
||||
.mode = .cursor,
|
||||
.grid_pos = .{ 2, 3 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.setCursor(cursor_cell);
|
||||
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
|
||||
|
||||
// And remove it.
|
||||
c.setCursor(null);
|
||||
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
|
||||
}
|
||||
|
||||
test "Contents clear retains other content" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
|
||||
var c: Contents = .{};
|
||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||
defer c.deinit(alloc);
|
||||
|
||||
// Set some contents
|
||||
// bg and fg cells in row 1
|
||||
const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_1: shaderpkg.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 1 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(1, 4).* = bg_cell_1;
|
||||
try c.add(alloc, .text, fg_cell_1);
|
||||
// bg and fg cells in row 2
|
||||
const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_2: shaderpkg.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 2 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(2, 4).* = bg_cell_2;
|
||||
try c.add(alloc, .text, fg_cell_2);
|
||||
|
||||
// Clear row 1, this should leave row 2 untouched
|
||||
c.clear(1);
|
||||
|
||||
// Row 2 should still contain its cells.
|
||||
try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
|
||||
// Fg row index is +1 because of cursor list at start
|
||||
try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
|
||||
}
|
||||
|
||||
test "Contents clear last added content" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
|
||||
var c: Contents = .{};
|
||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||
defer c.deinit(alloc);
|
||||
|
||||
// Set some contents
|
||||
// bg and fg cells in row 1
|
||||
const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_1: shaderpkg.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 1 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(1, 4).* = bg_cell_1;
|
||||
try c.add(alloc, .text, fg_cell_1);
|
||||
// bg and fg cells in row 2
|
||||
const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_2: shaderpkg.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 2 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(2, 4).* = bg_cell_2;
|
||||
try c.add(alloc, .text, fg_cell_2);
|
||||
|
||||
// Clear row 2, this should leave row 1 untouched
|
||||
c.clear(2);
|
||||
|
||||
// Row 1 should still contain its cells.
|
||||
try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
|
||||
// Fg row index is +1 because of cursor list at start
|
||||
try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
|
||||
}
|
||||
|
@@ -11,8 +11,13 @@ const renderer = @import("../renderer.zig");
|
||||
const math = @import("../math.zig");
|
||||
const Surface = @import("../Surface.zig");
|
||||
const link = @import("link.zig");
|
||||
const fgMode = @import("cell.zig").fgMode;
|
||||
const isCovering = @import("cell.zig").isCovering;
|
||||
const cellpkg = @import("cell.zig");
|
||||
const fgMode = cellpkg.fgMode;
|
||||
const isCovering = cellpkg.isCovering;
|
||||
const imagepkg = @import("image.zig");
|
||||
const Image = imagepkg.Image;
|
||||
const ImageMap = imagepkg.ImageMap;
|
||||
const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
|
||||
const shadertoy = @import("shadertoy.zig");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
@@ -71,21 +76,16 @@ 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 Texture = GraphicsAPI.Texture;
|
||||
const RenderPass = GraphicsAPI.RenderPass;
|
||||
|
||||
const shaderpkg = GraphicsAPI.shaders;
|
||||
|
||||
const cellpkg = GraphicsAPI.cellpkg;
|
||||
const imagepkg = GraphicsAPI.imagepkg;
|
||||
const Image = imagepkg.Image;
|
||||
const ImageMap = imagepkg.ImageMap;
|
||||
|
||||
const Shaders = shaderpkg.Shaders;
|
||||
|
||||
const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
|
||||
|
||||
/// Allocator that can be used
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
@@ -301,7 +301,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
/// Custom shader state, this is null if we have no custom shaders.
|
||||
custom_shader_state: ?CustomShaderState = null,
|
||||
|
||||
/// A buffer containing the uniform data.
|
||||
const UniformBuffer = Buffer(shaderpkg.Uniforms);
|
||||
const CellBgBuffer = Buffer(shaderpkg.CellBg);
|
||||
const CellTextBuffer = Buffer(shaderpkg.CellText);
|
||||
@@ -337,7 +336,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
const color = try api.initAtlasTexture(&.{
|
||||
.data = undefined,
|
||||
.size = 1,
|
||||
.format = .rgba,
|
||||
.format = .bgra,
|
||||
});
|
||||
errdefer color.deinit();
|
||||
|
||||
@@ -395,12 +394,20 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
front_texture: Texture,
|
||||
back_texture: Texture,
|
||||
|
||||
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(
|
||||
@@ -417,15 +424,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
null,
|
||||
);
|
||||
errdefer back_texture.deinit();
|
||||
|
||||
return .{
|
||||
.front_texture = front_texture,
|
||||
.back_texture = back_texture,
|
||||
.uniforms = uniforms,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CustomShaderState) void {
|
||||
self.front_texture.deinit();
|
||||
self.back_texture.deinit();
|
||||
self.uniforms.deinit();
|
||||
}
|
||||
|
||||
pub fn resize(
|
||||
@@ -1324,32 +1334,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
// Upload images to the GPU as necessary.
|
||||
{
|
||||
var image_it = self.images.iterator();
|
||||
while (image_it.next()) |kv| {
|
||||
switch (kv.value_ptr.image) {
|
||||
.ready => {},
|
||||
|
||||
.pending_gray,
|
||||
.pending_gray_alpha,
|
||||
.pending_rgb,
|
||||
.pending_rgba,
|
||||
.replace_gray,
|
||||
.replace_gray_alpha,
|
||||
.replace_rgb,
|
||||
.replace_rgba,
|
||||
=> try kv.value_ptr.image.upload(self.alloc, &self.api),
|
||||
|
||||
.unload_pending,
|
||||
.unload_replace,
|
||||
.unload_ready,
|
||||
=> {
|
||||
kv.value_ptr.image.deinit(self.alloc);
|
||||
self.images.removeByPtr(kv.key_ptr);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
try self.uploadKittyImages();
|
||||
|
||||
// Update custom shader uniforms if necessary.
|
||||
try self.updateCustomShaderUniforms();
|
||||
@@ -1391,23 +1376,43 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}});
|
||||
defer pass.complete();
|
||||
|
||||
// bg images
|
||||
try self.drawImagePlacements(&pass, self.image_placements.items[0..self.image_bg_end]);
|
||||
// bg
|
||||
// First we draw the background color.
|
||||
//
|
||||
// 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.
|
||||
pass.step(.{
|
||||
.pipeline = self.shaders.cell_bg_pipeline,
|
||||
.pipeline = self.shaders.pipelines.bg_color,
|
||||
.uniforms = frame.uniforms.buffer,
|
||||
.buffers = &.{ null, frame.cells_bg.buffer },
|
||||
.draw = .{
|
||||
.type = .triangle,
|
||||
.vertex_count = 3,
|
||||
},
|
||||
.draw = .{ .type = .triangle, .vertex_count = 3 },
|
||||
});
|
||||
// mg images
|
||||
try self.drawImagePlacements(&pass, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
||||
// text
|
||||
|
||||
// Then we draw any kitty images that need
|
||||
// to be behind text AND cell backgrounds.
|
||||
try self.drawImagePlacements(
|
||||
&pass,
|
||||
self.image_placements.items[0..self.image_bg_end],
|
||||
);
|
||||
|
||||
// Then we draw any opaque cell backgrounds.
|
||||
pass.step(.{
|
||||
.pipeline = self.shaders.cell_text_pipeline,
|
||||
.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.
|
||||
try self.drawImagePlacements(
|
||||
&pass,
|
||||
self.image_placements.items[self.image_bg_end..self.image_text_end],
|
||||
);
|
||||
|
||||
// Text.
|
||||
pass.step(.{
|
||||
.pipeline = self.shaders.pipelines.cell_text,
|
||||
.uniforms = frame.uniforms.buffer,
|
||||
.buffers = &.{
|
||||
frame.cells.buffer,
|
||||
@@ -1423,20 +1428,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
.instance_count = fg_count,
|
||||
},
|
||||
});
|
||||
// fg images
|
||||
try self.drawImagePlacements(&pass, self.image_placements.items[self.image_text_end..]);
|
||||
|
||||
// Kitty images in front of text.
|
||||
try self.drawImagePlacements(
|
||||
&pass,
|
||||
self.image_placements.items[self.image_text_end..],
|
||||
);
|
||||
}
|
||||
|
||||
// If we have custom shaders, then we render them.
|
||||
if (frame.custom_shader_state) |*state| {
|
||||
// We create a buffer on the GPU for our post uniforms.
|
||||
// TODO: This should be a part of the frame state tbqh.
|
||||
const PostBuffer = Buffer(shadertoy.Uniforms);
|
||||
const uniform_buffer = try PostBuffer.initFill(
|
||||
self.api.bufferOptions(),
|
||||
&.{self.custom_shader_uniforms},
|
||||
);
|
||||
defer uniform_buffer.deinit();
|
||||
// Sync our uniforms.
|
||||
try state.uniforms.sync(&.{self.custom_shader_uniforms});
|
||||
|
||||
for (self.shaders.post_pipelines, 0..) |pipeline, i| {
|
||||
defer state.swap();
|
||||
@@ -1452,7 +1455,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
pass.step(.{
|
||||
.pipeline = pipeline,
|
||||
.uniforms = uniform_buffer.buffer,
|
||||
.uniforms = state.uniforms.buffer,
|
||||
.textures = &.{state.back_texture},
|
||||
.draw = .{
|
||||
.type = .triangle,
|
||||
@@ -1539,7 +1542,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
defer buf.deinit();
|
||||
|
||||
pass.step(.{
|
||||
.pipeline = self.shaders.image_pipeline,
|
||||
.pipeline = self.shaders.pipelines.image,
|
||||
.buffers = &.{buf.buffer},
|
||||
.textures = &.{texture},
|
||||
.draw = .{
|
||||
@@ -1551,8 +1554,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// placements we need to render on our viewport.
|
||||
fn prepKittyGraphics(
|
||||
self: *Self,
|
||||
t: *terminal.Terminal,
|
||||
@@ -1589,7 +1591,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
|
||||
const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y;
|
||||
|
||||
// Go through the placements and ensure the image is loaded on the GPU.
|
||||
// Go through the placements and ensure the image is
|
||||
// on the GPU or else is ready to be sent to the GPU.
|
||||
var it = storage.placements.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const p = kv.value_ptr;
|
||||
@@ -1648,8 +1651,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}.lessThan,
|
||||
);
|
||||
|
||||
// Find our indices. The values are sorted by z so we can find the
|
||||
// first placement out of bounds to find the limits.
|
||||
// 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;
|
||||
@@ -1662,8 +1665,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
self.image_bg_end = bg_end orelse 0;
|
||||
self.image_text_end = text_end orelse self.image_bg_end;
|
||||
// If we didn't see any images with a z > the bg limit,
|
||||
// then our bg end is the end of our placement list.
|
||||
self.image_bg_end =
|
||||
bg_end orelse @intCast(self.image_placements.items.len);
|
||||
|
||||
// Same idea for the image_text_end.
|
||||
self.image_text_end =
|
||||
text_end orelse @intCast(self.image_placements.items.len);
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
@@ -1704,7 +1713,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Send our image to the GPU and store the placement for rendering.
|
||||
// Prepare the image for the GPU and store the placement.
|
||||
try self.prepKittyImage(&image);
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
@@ -1722,6 +1731,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the viewport-relative position for this
|
||||
/// placement and add it to the placements list.
|
||||
fn prepKittyPlacement(
|
||||
self: *Self,
|
||||
t: *terminal.Terminal,
|
||||
@@ -1785,6 +1796,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare the provided image for upload to the GPU by copying its
|
||||
/// data with our allocator and setting it to the pending state.
|
||||
fn prepKittyImage(
|
||||
self: *Self,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
@@ -1832,6 +1845,35 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
gop.value_ptr.transmit_time = image.transmit_time;
|
||||
}
|
||||
|
||||
/// Upload any images to the GPU that need to be uploaded,
|
||||
/// and remove any images that are no longer needed on the GPU.
|
||||
fn uploadKittyImages(self: *Self) !void {
|
||||
var image_it = self.images.iterator();
|
||||
while (image_it.next()) |kv| {
|
||||
switch (kv.value_ptr.image) {
|
||||
.ready => {},
|
||||
|
||||
.pending_gray,
|
||||
.pending_gray_alpha,
|
||||
.pending_rgb,
|
||||
.pending_rgba,
|
||||
.replace_gray,
|
||||
.replace_gray_alpha,
|
||||
.replace_rgb,
|
||||
.replace_rgba,
|
||||
=> try kv.value_ptr.image.upload(self.alloc, &self.api),
|
||||
|
||||
.unload_pending,
|
||||
.unload_replace,
|
||||
.unload_ready,
|
||||
=> {
|
||||
kv.value_ptr.image.deinit(self.alloc);
|
||||
self.images.removeByPtr(kv.key_ptr);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the configuration.
|
||||
pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
|
||||
self.draw_mutex.lock();
|
||||
@@ -2378,8 +2420,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
const bg_alpha: u8 = bg_alpha: {
|
||||
const default: u8 = 255;
|
||||
|
||||
if (self.config.background_opacity >= 1) break :bg_alpha default;
|
||||
|
||||
// Cells that are selected should be fully opaque.
|
||||
if (selected) break :bg_alpha default;
|
||||
|
||||
@@ -2387,12 +2427,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
if (style.flags.inverse) break :bg_alpha default;
|
||||
|
||||
// Cells that have an explicit bg color should be fully opaque.
|
||||
if (bg_style != null) {
|
||||
break :bg_alpha default;
|
||||
}
|
||||
if (bg_style != null) break :bg_alpha default;
|
||||
|
||||
// Otherwise, we use the configured background opacity.
|
||||
break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
|
||||
// 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).* = .{
|
||||
@@ -2769,7 +2809,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode: shaderpkg.CellText.Mode = switch (try fgMode(
|
||||
const mode: shaderpkg.CellText.Mode = switch (fgMode(
|
||||
render.presentation,
|
||||
cell_pin,
|
||||
)) {
|
||||
|
@@ -1,16 +1,14 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const objc = @import("objc");
|
||||
const wuffs = @import("wuffs");
|
||||
|
||||
const Metal = @import("../Metal.zig");
|
||||
const Texture = Metal.Texture;
|
||||
const Renderer = @import("../renderer.zig").Renderer;
|
||||
const GraphicsAPI = Renderer.API;
|
||||
const Texture = GraphicsAPI.Texture;
|
||||
|
||||
const mtl = @import("api.zig");
|
||||
|
||||
/// Represents a single image placement on the grid. A placement is a
|
||||
/// request to render an instance of an image.
|
||||
/// Represents a single image placement on the grid.
|
||||
/// A placement is a request to render an instance of an image.
|
||||
pub const Placement = struct {
|
||||
/// The image being rendered. This MUST be in the image map.
|
||||
image_id: u32,
|
||||
@@ -174,8 +172,8 @@ pub const Image = union(enum) {
|
||||
// scenarios where there is no existing texture and we can modify
|
||||
// the self pointer directly.
|
||||
const existing: Texture = switch (self.*) {
|
||||
// For pending, we can free the old data and become pending
|
||||
// ourselves.
|
||||
// For pending, we can free the old
|
||||
// data and become pending ourselves.
|
||||
.pending_gray => |p| {
|
||||
alloc.free(p.dataSlice(1));
|
||||
self.* = img;
|
||||
@@ -214,8 +212,8 @@ pub const Image = union(enum) {
|
||||
break :existing r[1];
|
||||
},
|
||||
|
||||
// If we were already pending a replacement, then we free our
|
||||
// existing pending data and use the same texture.
|
||||
// If we were already pending a replacement, then we free
|
||||
// our existing pending data and use the same texture.
|
||||
.replace_gray => |r| existing: {
|
||||
alloc.free(r.pending.dataSlice(1));
|
||||
break :existing r.texture;
|
||||
@@ -236,9 +234,9 @@ pub const Image = union(enum) {
|
||||
break :existing r.texture;
|
||||
},
|
||||
|
||||
// For both ready and unload_ready, we need to replace the
|
||||
// texture. We can't do that here, so we just mark ourselves
|
||||
// for replacement.
|
||||
// For both ready and unload_ready, we need to replace
|
||||
// the texture. We can't do that here, so we just mark
|
||||
// ourselves for replacement.
|
||||
.ready, .unload_ready => |tex| tex,
|
||||
};
|
||||
|
||||
@@ -281,6 +279,8 @@ pub const Image = union(enum) {
|
||||
=> true,
|
||||
|
||||
.ready,
|
||||
.pending_gray,
|
||||
.pending_gray_alpha,
|
||||
.pending_rgb,
|
||||
.pending_rgba,
|
||||
=> false,
|
||||
@@ -290,7 +290,10 @@ pub const Image = union(enum) {
|
||||
/// Converts the image data to a format that can be uploaded to the GPU.
|
||||
/// If the data is already in a format that can be uploaded, this is a
|
||||
/// no-op.
|
||||
pub fn convert(self: *Image, alloc: Allocator) !void {
|
||||
pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
|
||||
// As things stand, we currently convert all images to RGBA before
|
||||
// uploading to the GPU. This just makes things easier. In the future
|
||||
// we may want to support other formats.
|
||||
switch (self.*) {
|
||||
.ready,
|
||||
.unload_pending,
|
||||
@@ -302,8 +305,6 @@ pub const Image = union(enum) {
|
||||
.replace_rgba,
|
||||
=> {}, // ready
|
||||
|
||||
// RGB needs to be converted to RGBA because Metal textures
|
||||
// don't support RGB.
|
||||
.pending_rgb => |*p| {
|
||||
const data = p.dataSlice(3);
|
||||
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
|
||||
@@ -320,7 +321,6 @@ pub const Image = union(enum) {
|
||||
self.* = .{ .replace_rgba = r.* };
|
||||
},
|
||||
|
||||
// Gray and Gray+Alpha need to be converted to RGBA, too.
|
||||
.pending_gray => |*p| {
|
||||
const data = p.dataSlice(1);
|
||||
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
|
||||
@@ -360,11 +360,8 @@ pub const Image = union(enum) {
|
||||
pub fn upload(
|
||||
self: *Image,
|
||||
alloc: Allocator,
|
||||
metal: *const Metal,
|
||||
api: *const GraphicsAPI,
|
||||
) !void {
|
||||
const device = metal.device;
|
||||
const storage_mode = metal.default_storage_mode;
|
||||
|
||||
// Convert our data if we have to
|
||||
try self.convert(alloc);
|
||||
|
||||
@@ -373,15 +370,7 @@ pub const Image = union(enum) {
|
||||
|
||||
// Create our texture
|
||||
const texture = try Texture.init(
|
||||
.{
|
||||
.device = device,
|
||||
.pixel_format = .rgba8unorm_srgb,
|
||||
.resource_options = .{
|
||||
// Indicate that the CPU writes to this resource but never reads it.
|
||||
.cpu_cache_mode = .write_combined,
|
||||
.storage_mode = storage_mode,
|
||||
},
|
||||
},
|
||||
api.imageTextureOptions(.rgba, true),
|
||||
@intCast(p.width),
|
||||
@intCast(p.height),
|
||||
p.data[0 .. p.width * p.height * self.depth()],
|
@@ -1,358 +0,0 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const renderer = @import("../../renderer.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const mtl_shaders = @import("shaders.zig");
|
||||
|
||||
/// The possible cell content keys that exist.
|
||||
pub const Key = enum {
|
||||
bg,
|
||||
text,
|
||||
underline,
|
||||
strikethrough,
|
||||
overline,
|
||||
|
||||
/// Returns the GPU vertex type for this key.
|
||||
pub fn CellType(self: Key) type {
|
||||
return switch (self) {
|
||||
.bg => mtl_shaders.CellBg,
|
||||
|
||||
.text,
|
||||
.underline,
|
||||
.strikethrough,
|
||||
.overline,
|
||||
=> mtl_shaders.CellText,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// A pool of ArrayLists with methods for bulk operations.
|
||||
fn ArrayListPool(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = ArrayListPool(T);
|
||||
const ArrayListT = std.ArrayListUnmanaged(T);
|
||||
|
||||
// An array containing the lists that belong to this pool.
|
||||
lists: []ArrayListT = &[_]ArrayListT{},
|
||||
|
||||
// The pool will be initialized with empty ArrayLists.
|
||||
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
|
||||
const self: Self = .{
|
||||
.lists = try alloc.alloc(ArrayListT, list_count),
|
||||
};
|
||||
|
||||
for (self.lists) |*list| {
|
||||
list.* = try .initCapacity(alloc, initial_capacity);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, alloc: Allocator) void {
|
||||
for (self.lists) |*list| {
|
||||
list.deinit(alloc);
|
||||
}
|
||||
alloc.free(self.lists);
|
||||
}
|
||||
|
||||
/// Clear all lists in the pool.
|
||||
pub fn reset(self: *Self) void {
|
||||
for (self.lists) |*list| {
|
||||
list.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The contents of all the cells in the terminal.
|
||||
///
|
||||
/// The goal of this data structure is to allow for efficient row-wise
|
||||
/// clearing of data from the GPU buffers, to allow for row-wise dirty
|
||||
/// tracking to eliminate the overhead of rebuilding the GPU buffers
|
||||
/// each frame.
|
||||
///
|
||||
/// Must be initialized by resizing before calling any operations.
|
||||
pub const Contents = struct {
|
||||
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
|
||||
|
||||
/// Flat array containing cell background colors for the terminal grid.
|
||||
///
|
||||
/// Indexed as `bg_cells[row * size.columns + col]`.
|
||||
///
|
||||
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
|
||||
/// of directly indexing in order to avoid integer size bugs.
|
||||
bg_cells: []mtl_shaders.CellBg = undefined,
|
||||
|
||||
/// The ArrayListPool which holds all of the foreground cells. When sized
|
||||
/// with Contents.resize the individual ArrayLists are given enough room
|
||||
/// that they can hold a single row with #cols glyphs, underlines, and
|
||||
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
|
||||
/// it is possible to exceed this with combining glyphs that add a glyph
|
||||
/// but take up no column since they combine with the previous one, as
|
||||
/// well as with fonts that perform multi-substitutions for glyphs, which
|
||||
/// can result in a similar situation where multiple glyphs reside in the
|
||||
/// same column.
|
||||
///
|
||||
/// Allocations should nevertheless be exceedingly rare since hitting the
|
||||
/// initial capacity of a list would require a row filled with underlined
|
||||
/// struck through characters, at least one of which is a multi-glyph
|
||||
/// composite.
|
||||
///
|
||||
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
|
||||
/// the pool is reserved for the cursor, which must be the first item in
|
||||
/// the buffer.
|
||||
///
|
||||
/// Must be initialized by calling resize on the Contents struct before
|
||||
/// calling any operations.
|
||||
fg_rows: ArrayListPool(mtl_shaders.CellText) = .{},
|
||||
|
||||
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
||||
alloc.free(self.bg_cells);
|
||||
self.fg_rows.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Resize the cell contents for the given grid size. This will
|
||||
/// always invalidate the entire cell contents.
|
||||
pub fn resize(
|
||||
self: *Contents,
|
||||
alloc: Allocator,
|
||||
size: renderer.GridSize,
|
||||
) !void {
|
||||
self.size = size;
|
||||
|
||||
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
|
||||
|
||||
const bg_cells = try alloc.alloc(mtl_shaders.CellBg, cell_count);
|
||||
errdefer alloc.free(bg_cells);
|
||||
|
||||
@memset(bg_cells, .{ 0, 0, 0, 0 });
|
||||
|
||||
// The foreground lists can hold 3 types of items:
|
||||
// - Glyphs
|
||||
// - Underlines
|
||||
// - Strikethroughs
|
||||
// So we give them an initial capacity of size.columns * 3, which will
|
||||
// avoid any further allocations in the vast majority of cases. Sadly
|
||||
// we can not assume capacity though, since with combining glyphs that
|
||||
// form a single grapheme, and multi-substitutions in fonts, the number
|
||||
// of glyphs in a row is theoretically unlimited.
|
||||
//
|
||||
// We have size.rows + 1 lists because index 0 is used for a special
|
||||
// list containing the cursor cell which needs to be first in the buffer.
|
||||
var fg_rows = try ArrayListPool(mtl_shaders.CellText).init(alloc, size.rows + 1, size.columns * 3);
|
||||
errdefer fg_rows.deinit(alloc);
|
||||
|
||||
alloc.free(self.bg_cells);
|
||||
self.fg_rows.deinit(alloc);
|
||||
|
||||
self.bg_cells = bg_cells;
|
||||
self.fg_rows = fg_rows;
|
||||
|
||||
// We don't need 3*cols worth of cells for the cursor list, so we can
|
||||
// replace it with a smaller list. This is technically a tiny bit of
|
||||
// extra work but resize is not a hot function so it's worth it to not
|
||||
// waste the memory.
|
||||
self.fg_rows.lists[0].deinit(alloc);
|
||||
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(mtl_shaders.CellText).initCapacity(alloc, 1);
|
||||
}
|
||||
|
||||
/// Reset the cell contents to an empty state without resizing.
|
||||
pub fn reset(self: *Contents) void {
|
||||
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
|
||||
self.fg_rows.reset();
|
||||
}
|
||||
|
||||
/// Set the cursor value. If the value is null then the cursor is hidden.
|
||||
pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void {
|
||||
self.fg_rows.lists[0].clearRetainingCapacity();
|
||||
|
||||
if (v) |cell| {
|
||||
self.fg_rows.lists[0].appendAssumeCapacity(cell);
|
||||
}
|
||||
}
|
||||
|
||||
/// Access a background cell. Prefer this function over direct indexing
|
||||
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
|
||||
pub inline fn bgCell(self: *Contents, row: usize, col: usize) *mtl_shaders.CellBg {
|
||||
return &self.bg_cells[row * self.size.columns + col];
|
||||
}
|
||||
|
||||
/// Add a cell to the appropriate list. Adding the same cell twice will
|
||||
/// result in duplication in the vertex buffer. The caller should clear
|
||||
/// the corresponding row with Contents.clear to remove old cells first.
|
||||
pub fn add(
|
||||
self: *Contents,
|
||||
alloc: Allocator,
|
||||
comptime key: Key,
|
||||
cell: key.CellType(),
|
||||
) !void {
|
||||
const y = cell.grid_pos[1];
|
||||
|
||||
assert(y < self.size.rows);
|
||||
|
||||
switch (key) {
|
||||
.bg => comptime unreachable,
|
||||
|
||||
.text,
|
||||
.underline,
|
||||
.strikethrough,
|
||||
.overline,
|
||||
// We have a special list containing the cursor cell at the start
|
||||
// of our fg row pool, so we need to add 1 to the y to get the
|
||||
// correct index.
|
||||
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all of the cell contents for a given row.
|
||||
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
||||
assert(y < self.size.rows);
|
||||
|
||||
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
|
||||
|
||||
// We have a special list containing the cursor cell at the start
|
||||
// of our fg row pool, so we need to add 1 to the y to get the
|
||||
// correct index.
|
||||
self.fg_rows.lists[y + 1].clearRetainingCapacity();
|
||||
}
|
||||
};
|
||||
|
||||
test Contents {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
|
||||
var c: Contents = .{};
|
||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||
defer c.deinit(alloc);
|
||||
|
||||
// We should start off empty after resizing.
|
||||
for (0..rows) |y| {
|
||||
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
|
||||
for (0..cols) |x| {
|
||||
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
|
||||
}
|
||||
}
|
||||
// And the cursor row should have a capacity of 1 and also be empty.
|
||||
try testing.expect(c.fg_rows.lists[0].capacity == 1);
|
||||
try testing.expect(c.fg_rows.lists[0].items.len == 0);
|
||||
|
||||
// Add some contents.
|
||||
const bg_cell: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell: mtl_shaders.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 1 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(1, 4).* = bg_cell;
|
||||
try c.add(alloc, .text, fg_cell);
|
||||
try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
|
||||
// The fg row index is offset by 1 because of the cursor list.
|
||||
try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
|
||||
|
||||
// And we should be able to clear it.
|
||||
c.clear(1);
|
||||
for (0..rows) |y| {
|
||||
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
|
||||
for (0..cols) |x| {
|
||||
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a cursor.
|
||||
const cursor_cell: mtl_shaders.CellText = .{
|
||||
.mode = .cursor,
|
||||
.grid_pos = .{ 2, 3 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.setCursor(cursor_cell);
|
||||
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
|
||||
|
||||
// And remove it.
|
||||
c.setCursor(null);
|
||||
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
|
||||
}
|
||||
|
||||
test "Contents clear retains other content" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
|
||||
var c: Contents = .{};
|
||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||
defer c.deinit(alloc);
|
||||
|
||||
// Set some contents
|
||||
// bg and fg cells in row 1
|
||||
const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_1: mtl_shaders.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 1 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(1, 4).* = bg_cell_1;
|
||||
try c.add(alloc, .text, fg_cell_1);
|
||||
// bg and fg cells in row 2
|
||||
const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_2: mtl_shaders.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 2 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(2, 4).* = bg_cell_2;
|
||||
try c.add(alloc, .text, fg_cell_2);
|
||||
|
||||
// Clear row 1, this should leave row 2 untouched
|
||||
c.clear(1);
|
||||
|
||||
// Row 2 should still contain its cells.
|
||||
try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
|
||||
// Fg row index is +1 because of cursor list at start
|
||||
try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
|
||||
}
|
||||
|
||||
test "Contents clear last added content" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
|
||||
var c: Contents = .{};
|
||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||
defer c.deinit(alloc);
|
||||
|
||||
// Set some contents
|
||||
// bg and fg cells in row 1
|
||||
const bg_cell_1: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_1: mtl_shaders.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 1 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(1, 4).* = bg_cell_1;
|
||||
try c.add(alloc, .text, fg_cell_1);
|
||||
// bg and fg cells in row 2
|
||||
const bg_cell_2: mtl_shaders.CellBg = .{ 0, 0, 0, 1 };
|
||||
const fg_cell_2: mtl_shaders.CellText = .{
|
||||
.mode = .fg,
|
||||
.grid_pos = .{ 4, 2 },
|
||||
.color = .{ 0, 0, 0, 1 },
|
||||
};
|
||||
c.bgCell(2, 4).* = bg_cell_2;
|
||||
try c.add(alloc, .text, fg_cell_2);
|
||||
|
||||
// Clear row 2, this should leave row 1 untouched
|
||||
c.clear(2);
|
||||
|
||||
// Row 1 should still contain its cells.
|
||||
try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
|
||||
// Fg row index is +1 because of cursor list at start
|
||||
try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
|
||||
}
|
@@ -10,20 +10,90 @@ const Pipeline = @import("Pipeline.zig");
|
||||
|
||||
const log = std.log.scoped(.metal);
|
||||
|
||||
const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
|
||||
&.{
|
||||
.{ "bg_color", .{
|
||||
.vertex_fn = "full_screen_vertex",
|
||||
.fragment_fn = "bg_color_fragment",
|
||||
.blending_enabled = false,
|
||||
} },
|
||||
.{ "cell_bg", .{
|
||||
.vertex_fn = "full_screen_vertex",
|
||||
.fragment_fn = "cell_bg_fragment",
|
||||
.blending_enabled = true,
|
||||
} },
|
||||
.{ "cell_text", .{
|
||||
.vertex_attributes = CellText,
|
||||
.vertex_fn = "cell_text_vertex",
|
||||
.fragment_fn = "cell_text_fragment",
|
||||
.step_fn = .per_instance,
|
||||
.blending_enabled = true,
|
||||
} },
|
||||
.{ "image", .{
|
||||
.vertex_attributes = Image,
|
||||
.vertex_fn = "image_vertex",
|
||||
.fragment_fn = "image_fragment",
|
||||
.step_fn = .per_instance,
|
||||
.blending_enabled = true,
|
||||
} },
|
||||
};
|
||||
|
||||
/// All the comptime-known info about a pipeline, so that
|
||||
/// we can define them ahead-of-time in an ergonomic way.
|
||||
const PipelineDescription = struct {
|
||||
vertex_attributes: ?type = null,
|
||||
vertex_fn: []const u8,
|
||||
fragment_fn: []const u8,
|
||||
step_fn: mtl.MTLVertexStepFunction = .per_vertex,
|
||||
blending_enabled: bool,
|
||||
|
||||
fn initPipeline(
|
||||
self: PipelineDescription,
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !Pipeline {
|
||||
return try .init(self.vertex_attributes, .{
|
||||
.device = device,
|
||||
.vertex_fn = self.vertex_fn,
|
||||
.fragment_fn = self.fragment_fn,
|
||||
.vertex_library = library,
|
||||
.fragment_library = library,
|
||||
.step_fn = self.step_fn,
|
||||
.attachments = &.{.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = self.blending_enabled,
|
||||
}},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// We create a type for the pipeline collection based on our desc array.
|
||||
const PipelineCollection = t: {
|
||||
var fields: [pipeline_descs.len]std.builtin.Type.StructField = undefined;
|
||||
for (pipeline_descs, 0..) |pipeline, i| {
|
||||
fields[i] = .{
|
||||
.name = pipeline[0],
|
||||
.type = Pipeline,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(Pipeline),
|
||||
};
|
||||
}
|
||||
break :t @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
};
|
||||
|
||||
/// This contains the state for the shaders used by the Metal renderer.
|
||||
pub const Shaders = struct {
|
||||
library: objc.Object,
|
||||
|
||||
/// Renders cell foreground elements (text, decorations).
|
||||
cell_text_pipeline: Pipeline,
|
||||
|
||||
/// The cell background shader is the shader used to render the
|
||||
/// background of terminal cells.
|
||||
cell_bg_pipeline: Pipeline,
|
||||
|
||||
/// The image shader is the shader used to render images for things
|
||||
/// like the Kitty image protocol.
|
||||
image_pipeline: Pipeline,
|
||||
/// Collection of available render pipelines.
|
||||
pipelines: PipelineCollection,
|
||||
|
||||
/// Custom shaders to run against the final drawable texture. This
|
||||
/// can be used to apply a lot of effects. Each shader is run in sequence
|
||||
@@ -48,14 +118,24 @@ pub const Shaders = struct {
|
||||
const library = try initLibrary(device);
|
||||
errdefer library.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
|
||||
errdefer cell_text_pipeline.deinit();
|
||||
var pipelines: PipelineCollection = undefined;
|
||||
|
||||
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
|
||||
errdefer cell_bg_pipeline.deinit();
|
||||
var initialized_pipelines: usize = 0;
|
||||
|
||||
const image_pipeline = try initImagePipeline(device, library, pixel_format);
|
||||
errdefer image_pipeline.deinit();
|
||||
errdefer inline for (pipeline_descs, 0..) |pipeline, i| {
|
||||
if (i < initialized_pipelines) {
|
||||
@field(pipelines, pipeline[0]).deinit();
|
||||
}
|
||||
};
|
||||
|
||||
inline for (pipeline_descs) |pipeline| {
|
||||
@field(pipelines, pipeline[0]) = try pipeline[1].initPipeline(
|
||||
device,
|
||||
library,
|
||||
pixel_format,
|
||||
);
|
||||
initialized_pipelines += 1;
|
||||
}
|
||||
|
||||
const post_pipelines: []const Pipeline = initPostPipelines(
|
||||
alloc,
|
||||
@@ -77,9 +157,7 @@ pub const Shaders = struct {
|
||||
|
||||
return .{
|
||||
.library = library,
|
||||
.cell_text_pipeline = cell_text_pipeline,
|
||||
.cell_bg_pipeline = cell_bg_pipeline,
|
||||
.image_pipeline = image_pipeline,
|
||||
.pipelines = pipelines,
|
||||
.post_pipelines = post_pipelines,
|
||||
};
|
||||
}
|
||||
@@ -89,9 +167,9 @@ pub const Shaders = struct {
|
||||
self.defunct = true;
|
||||
|
||||
// Release our primary shaders
|
||||
self.cell_text_pipeline.deinit();
|
||||
self.cell_bg_pipeline.deinit();
|
||||
self.image_pipeline.deinit();
|
||||
inline for (pipeline_descs) |pipeline| {
|
||||
@field(self.pipelines, pipeline[0]).deinit();
|
||||
}
|
||||
self.library.msgSend(void, objc.sel("release"), .{});
|
||||
|
||||
// Release our postprocess shaders
|
||||
@@ -104,15 +182,7 @@ pub const Shaders = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
pub const Image = extern struct {
|
||||
grid_pos: [2]f32,
|
||||
cell_offset: [2]f32,
|
||||
source_rect: [4]f32,
|
||||
dest_size: [2]f32,
|
||||
};
|
||||
|
||||
/// The uniforms that are passed to the terminal cell shader.
|
||||
/// The uniforms that are passed to our shaders.
|
||||
pub const Uniforms = extern struct {
|
||||
// Note: all of the explicit alignments are copied from the
|
||||
// MSL developer reference just so that we can be sure that we got
|
||||
@@ -182,6 +252,42 @@ pub const Uniforms = extern struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// This is a single parameter for the terminal cell shader.
|
||||
pub const CellText = extern struct {
|
||||
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
|
||||
glyph_size: [2]u32 align(8) = .{ 0, 0 },
|
||||
bearings: [2]i16 align(4) = .{ 0, 0 },
|
||||
grid_pos: [2]u16 align(4),
|
||||
color: [4]u8 align(4),
|
||||
mode: Mode align(1),
|
||||
constraint_width: u8 align(1) = 0,
|
||||
|
||||
pub const Mode = enum(u8) {
|
||||
fg = 1,
|
||||
fg_constrained = 2,
|
||||
fg_color = 3,
|
||||
cursor = 4,
|
||||
fg_powerline = 5,
|
||||
};
|
||||
|
||||
test {
|
||||
// Minimizing the size of this struct is important,
|
||||
// so we test it in order to be aware of any changes.
|
||||
try std.testing.expectEqual(32, @sizeOf(CellText));
|
||||
}
|
||||
};
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub const CellBg = [4]u8;
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
pub const Image = extern struct {
|
||||
grid_pos: [2]f32,
|
||||
cell_offset: [2]f32,
|
||||
source_rect: [4]f32,
|
||||
dest_size: [2]f32,
|
||||
};
|
||||
|
||||
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
|
||||
fn initLibrary(device: objc.Object) !objc.Object {
|
||||
const start = try std.time.Instant.now();
|
||||
@@ -294,99 +400,6 @@ fn initPostPipeline(
|
||||
});
|
||||
}
|
||||
|
||||
/// This is a single parameter for the terminal cell shader.
|
||||
pub const CellText = extern struct {
|
||||
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
|
||||
glyph_size: [2]u32 align(8) = .{ 0, 0 },
|
||||
bearings: [2]i16 align(4) = .{ 0, 0 },
|
||||
grid_pos: [2]u16 align(4),
|
||||
color: [4]u8 align(4),
|
||||
mode: Mode align(1),
|
||||
constraint_width: u8 align(1) = 0,
|
||||
|
||||
pub const Mode = enum(u8) {
|
||||
fg = 1,
|
||||
fg_constrained = 2,
|
||||
fg_color = 3,
|
||||
cursor = 4,
|
||||
fg_powerline = 5,
|
||||
};
|
||||
|
||||
test {
|
||||
// Minimizing the size of this struct is important,
|
||||
// so we test it in order to be aware of any changes.
|
||||
try std.testing.expectEqual(32, @sizeOf(CellText));
|
||||
}
|
||||
};
|
||||
|
||||
/// Initialize the cell render pipeline for our shader library.
|
||||
fn initCellTextPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !Pipeline {
|
||||
return try Pipeline.init(CellText, .{
|
||||
.device = device,
|
||||
.vertex_fn = "cell_text_vertex",
|
||||
.fragment_fn = "cell_text_fragment",
|
||||
.vertex_library = library,
|
||||
.fragment_library = library,
|
||||
.step_fn = .per_instance,
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub const CellBg = [4]u8;
|
||||
|
||||
/// Initialize the cell background render pipeline for our shader library.
|
||||
fn initCellBgPipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !Pipeline {
|
||||
return try Pipeline.init(null, .{
|
||||
.device = device,
|
||||
.vertex_fn = "cell_bg_vertex",
|
||||
.fragment_fn = "cell_bg_fragment",
|
||||
.vertex_library = library,
|
||||
.fragment_library = library,
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize the image render pipeline for our shader library.
|
||||
fn initImagePipeline(
|
||||
device: objc.Object,
|
||||
library: objc.Object,
|
||||
pixel_format: mtl.MTLPixelFormat,
|
||||
) !Pipeline {
|
||||
return try Pipeline.init(Image, .{
|
||||
.device = device,
|
||||
.vertex_fn = "image_vertex",
|
||||
.fragment_fn = "image_fragment",
|
||||
.vertex_library = library,
|
||||
.fragment_library = library,
|
||||
.step_fn = .per_instance,
|
||||
.attachments = &.{
|
||||
.{
|
||||
.pixel_format = pixel_format,
|
||||
.blending_enabled = true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn checkError(err_: ?*anyopaque) !void {
|
||||
const nserr = objc.Object.fromId(err_ orelse return);
|
||||
const str = @as(
|
||||
|
@@ -1,220 +0,0 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const renderer = @import("../../renderer.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const shaderpkg = @import("shaders.zig");
|
||||
|
||||
/// The possible cell content keys that exist.
|
||||
pub const Key = enum {
|
||||
bg,
|
||||
text,
|
||||
underline,
|
||||
strikethrough,
|
||||
overline,
|
||||
|
||||
/// Returns the GPU vertex type for this key.
|
||||
pub fn CellType(self: Key) type {
|
||||
return switch (self) {
|
||||
.bg => shaderpkg.CellBg,
|
||||
|
||||
.text,
|
||||
.underline,
|
||||
.strikethrough,
|
||||
.overline,
|
||||
=> shaderpkg.CellText,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// A pool of ArrayLists with methods for bulk operations.
|
||||
fn ArrayListPool(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = ArrayListPool(T);
|
||||
const ArrayListT = std.ArrayListUnmanaged(T);
|
||||
|
||||
// An array containing the lists that belong to this pool.
|
||||
lists: []ArrayListT = &[_]ArrayListT{},
|
||||
|
||||
// The pool will be initialized with empty ArrayLists.
|
||||
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
|
||||
const self: Self = .{
|
||||
.lists = try alloc.alloc(ArrayListT, list_count),
|
||||
};
|
||||
|
||||
for (self.lists) |*list| {
|
||||
list.* = try ArrayListT.initCapacity(alloc, initial_capacity);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, alloc: Allocator) void {
|
||||
for (self.lists) |*list| {
|
||||
list.deinit(alloc);
|
||||
}
|
||||
alloc.free(self.lists);
|
||||
}
|
||||
|
||||
/// Clear all lists in the pool.
|
||||
pub fn reset(self: *Self) void {
|
||||
for (self.lists) |*list| {
|
||||
list.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The contents of all the cells in the terminal.
|
||||
///
|
||||
/// The goal of this data structure is to allow for efficient row-wise
|
||||
/// clearing of data from the GPU buffers, to allow for row-wise dirty
|
||||
/// tracking to eliminate the overhead of rebuilding the GPU buffers
|
||||
/// each frame.
|
||||
///
|
||||
/// Must be initialized by resizing before calling any operations.
|
||||
pub const Contents = struct {
|
||||
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
|
||||
|
||||
/// Flat array containing cell background colors for the terminal grid.
|
||||
///
|
||||
/// Indexed as `bg_cells[row * size.columns + col]`.
|
||||
///
|
||||
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
|
||||
/// of directly indexing in order to avoid integer size bugs.
|
||||
bg_cells: []shaderpkg.CellBg = undefined,
|
||||
|
||||
/// The ArrayListPool which holds all of the foreground cells. When sized
|
||||
/// with Contents.resize the individual ArrayLists are given enough room
|
||||
/// that they can hold a single row with #cols glyphs, underlines, and
|
||||
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
|
||||
/// it is possible to exceed this with combining glyphs that add a glyph
|
||||
/// but take up no column since they combine with the previous one, as
|
||||
/// well as with fonts that perform multi-substitutions for glyphs, which
|
||||
/// can result in a similar situation where multiple glyphs reside in the
|
||||
/// same column.
|
||||
///
|
||||
/// Allocations should nevertheless be exceedingly rare since hitting the
|
||||
/// initial capacity of a list would require a row filled with underlined
|
||||
/// struck through characters, at least one of which is a multi-glyph
|
||||
/// composite.
|
||||
///
|
||||
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
|
||||
/// the pool is reserved for the cursor, which must be the first item in
|
||||
/// the buffer.
|
||||
///
|
||||
/// Must be initialized by calling resize on the Contents struct before
|
||||
/// calling any operations.
|
||||
fg_rows: ArrayListPool(shaderpkg.CellText) = .{},
|
||||
|
||||
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
||||
alloc.free(self.bg_cells);
|
||||
self.fg_rows.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Resize the cell contents for the given grid size. This will
|
||||
/// always invalidate the entire cell contents.
|
||||
pub fn resize(
|
||||
self: *Contents,
|
||||
alloc: Allocator,
|
||||
size: renderer.GridSize,
|
||||
) !void {
|
||||
self.size = size;
|
||||
|
||||
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
|
||||
|
||||
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
|
||||
errdefer alloc.free(bg_cells);
|
||||
|
||||
@memset(bg_cells, .{ 0, 0, 0, 0 });
|
||||
|
||||
// The foreground lists can hold 3 types of items:
|
||||
// - Glyphs
|
||||
// - Underlines
|
||||
// - Strikethroughs
|
||||
// So we give them an initial capacity of size.columns * 3, which will
|
||||
// avoid any further allocations in the vast majority of cases. Sadly
|
||||
// we can not assume capacity though, since with combining glyphs that
|
||||
// form a single grapheme, and multi-substitutions in fonts, the number
|
||||
// of glyphs in a row is theoretically unlimited.
|
||||
//
|
||||
// We have size.rows + 1 lists because index 0 is used for a special
|
||||
// list containing the cursor cell which needs to be first in the buffer.
|
||||
var fg_rows = try ArrayListPool(shaderpkg.CellText).init(alloc, size.rows + 1, size.columns * 3);
|
||||
errdefer fg_rows.deinit(alloc);
|
||||
|
||||
alloc.free(self.bg_cells);
|
||||
self.fg_rows.deinit(alloc);
|
||||
|
||||
self.bg_cells = bg_cells;
|
||||
self.fg_rows = fg_rows;
|
||||
|
||||
// We don't need 3*cols worth of cells for the cursor list, so we can
|
||||
// replace it with a smaller list. This is technically a tiny bit of
|
||||
// extra work but resize is not a hot function so it's worth it to not
|
||||
// waste the memory.
|
||||
self.fg_rows.lists[0].deinit(alloc);
|
||||
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(shaderpkg.CellText).initCapacity(alloc, 1);
|
||||
}
|
||||
|
||||
/// Reset the cell contents to an empty state without resizing.
|
||||
pub fn reset(self: *Contents) void {
|
||||
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
|
||||
self.fg_rows.reset();
|
||||
}
|
||||
|
||||
/// Set the cursor value. If the value is null then the cursor is hidden.
|
||||
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
|
||||
self.fg_rows.lists[0].clearRetainingCapacity();
|
||||
|
||||
if (v) |cell| {
|
||||
self.fg_rows.lists[0].appendAssumeCapacity(cell);
|
||||
}
|
||||
}
|
||||
|
||||
/// Access a background cell. Prefer this function over direct indexing
|
||||
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
|
||||
pub inline fn bgCell(self: *Contents, row: usize, col: usize) *shaderpkg.CellBg {
|
||||
return &self.bg_cells[row * self.size.columns + col];
|
||||
}
|
||||
|
||||
/// Add a cell to the appropriate list. Adding the same cell twice will
|
||||
/// result in duplication in the vertex buffer. The caller should clear
|
||||
/// the corresponding row with Contents.clear to remove old cells first.
|
||||
pub fn add(
|
||||
self: *Contents,
|
||||
alloc: Allocator,
|
||||
comptime key: Key,
|
||||
cell: key.CellType(),
|
||||
) !void {
|
||||
const y = cell.grid_pos[1];
|
||||
|
||||
assert(y < self.size.rows);
|
||||
|
||||
switch (key) {
|
||||
.bg => comptime unreachable,
|
||||
|
||||
.text,
|
||||
.underline,
|
||||
.strikethrough,
|
||||
.overline,
|
||||
// We have a special list containing the cursor cell at the start
|
||||
// of our fg row pool, so we need to add 1 to the y to get the
|
||||
// correct index.
|
||||
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all of the cell contents for a given row.
|
||||
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
||||
assert(y < self.size.rows);
|
||||
|
||||
@memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
|
||||
|
||||
// We have a special list containing the cursor cell at the start
|
||||
// of our fg row pool, so we need to add 1 to the y to get the
|
||||
// correct index.
|
||||
self.fg_rows.lists[y + 1].clearRetainingCapacity();
|
||||
}
|
||||
};
|
@@ -1,423 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const gl = @import("opengl");
|
||||
const wuffs = @import("wuffs");
|
||||
const OpenGL = @import("../OpenGL.zig");
|
||||
const Texture = OpenGL.Texture;
|
||||
|
||||
/// Represents a single image placement on the grid. A placement is a
|
||||
/// request to render an instance of an image.
|
||||
pub const Placement = struct {
|
||||
/// The image being rendered. This MUST be in the image map.
|
||||
image_id: u32,
|
||||
|
||||
/// The grid x/y where this placement is located.
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
|
||||
/// The width/height of the placed image.
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
/// The offset in pixels from the top left of the cell. This is
|
||||
/// clamped to the size of a cell.
|
||||
cell_offset_x: u32,
|
||||
cell_offset_y: u32,
|
||||
|
||||
/// The source rectangle of the placement.
|
||||
source_x: u32,
|
||||
source_y: u32,
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
};
|
||||
|
||||
/// The map used for storing images.
|
||||
pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct {
|
||||
image: Image,
|
||||
transmit_time: std.time.Instant,
|
||||
});
|
||||
|
||||
/// The state for a single image that is to be rendered. The image can be
|
||||
/// pending upload or ready to use with a texture.
|
||||
pub const Image = union(enum) {
|
||||
/// The image is pending upload to the GPU. The different keys are
|
||||
/// different formats since some formats aren't accepted by the GPU
|
||||
/// and require conversion.
|
||||
///
|
||||
/// This data is owned by this union so it must be freed once the
|
||||
/// image is uploaded.
|
||||
pending_gray: Pending,
|
||||
pending_gray_alpha: Pending,
|
||||
pending_rgb: Pending,
|
||||
pending_rgba: Pending,
|
||||
|
||||
/// This is the same as the pending states but there is a texture
|
||||
/// already allocated that we want to replace.
|
||||
replace_gray: Replace,
|
||||
replace_gray_alpha: Replace,
|
||||
replace_rgb: Replace,
|
||||
replace_rgba: Replace,
|
||||
|
||||
/// The image is uploaded and ready to be used.
|
||||
ready: Texture,
|
||||
|
||||
/// The image is uploaded but is scheduled to be unloaded.
|
||||
unload_pending: []u8,
|
||||
unload_ready: Texture,
|
||||
unload_replace: struct { []u8, Texture },
|
||||
|
||||
pub const Replace = struct {
|
||||
texture: Texture,
|
||||
pending: Pending,
|
||||
};
|
||||
|
||||
/// Pending image data that needs to be uploaded to the GPU.
|
||||
pub const Pending = struct {
|
||||
height: u32,
|
||||
width: u32,
|
||||
|
||||
/// Data is always expected to be (width * height * depth). Depth
|
||||
/// is based on the union key.
|
||||
data: [*]u8,
|
||||
|
||||
pub fn dataSlice(self: Pending, d: u32) []u8 {
|
||||
return self.data[0..self.len(d)];
|
||||
}
|
||||
|
||||
pub fn len(self: Pending, d: u32) u32 {
|
||||
return self.width * self.height * d;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(self: Image, alloc: Allocator) void {
|
||||
switch (self) {
|
||||
.pending_gray => |p| alloc.free(p.dataSlice(1)),
|
||||
.pending_gray_alpha => |p| alloc.free(p.dataSlice(2)),
|
||||
.pending_rgb => |p| alloc.free(p.dataSlice(3)),
|
||||
.pending_rgba => |p| alloc.free(p.dataSlice(4)),
|
||||
.unload_pending => |data| alloc.free(data),
|
||||
|
||||
.replace_gray => |r| {
|
||||
alloc.free(r.pending.dataSlice(1));
|
||||
r.texture.deinit();
|
||||
},
|
||||
|
||||
.replace_gray_alpha => |r| {
|
||||
alloc.free(r.pending.dataSlice(2));
|
||||
r.texture.deinit();
|
||||
},
|
||||
|
||||
.replace_rgb => |r| {
|
||||
alloc.free(r.pending.dataSlice(3));
|
||||
r.texture.deinit();
|
||||
},
|
||||
|
||||
.replace_rgba => |r| {
|
||||
alloc.free(r.pending.dataSlice(4));
|
||||
r.texture.deinit();
|
||||
},
|
||||
|
||||
.unload_replace => |r| {
|
||||
alloc.free(r[0]);
|
||||
r[1].deinit();
|
||||
},
|
||||
|
||||
.ready,
|
||||
.unload_ready,
|
||||
=> |tex| tex.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark this image for unload whatever state it is in.
|
||||
pub fn markForUnload(self: *Image) void {
|
||||
self.* = switch (self.*) {
|
||||
.unload_pending,
|
||||
.unload_replace,
|
||||
.unload_ready,
|
||||
=> return,
|
||||
|
||||
.ready => |obj| .{ .unload_ready = obj },
|
||||
.pending_gray => |p| .{ .unload_pending = p.dataSlice(1) },
|
||||
.pending_gray_alpha => |p| .{ .unload_pending = p.dataSlice(2) },
|
||||
.pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) },
|
||||
.pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) },
|
||||
.replace_gray => |r| .{ .unload_replace = .{
|
||||
r.pending.dataSlice(1), r.texture,
|
||||
} },
|
||||
.replace_gray_alpha => |r| .{ .unload_replace = .{
|
||||
r.pending.dataSlice(2), r.texture,
|
||||
} },
|
||||
.replace_rgb => |r| .{ .unload_replace = .{
|
||||
r.pending.dataSlice(3), r.texture,
|
||||
} },
|
||||
.replace_rgba => |r| .{ .unload_replace = .{
|
||||
r.pending.dataSlice(4), r.texture,
|
||||
} },
|
||||
};
|
||||
}
|
||||
|
||||
/// Replace the currently pending image with a new one. This will
|
||||
/// attempt to update the existing texture if it is already allocated.
|
||||
/// If the texture is not allocated, this will act like a new upload.
|
||||
///
|
||||
/// This function only marks the image for replace. The actual logic
|
||||
/// to replace is done later.
|
||||
pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void {
|
||||
assert(img.pending() != null);
|
||||
|
||||
// Get our existing texture. This switch statement will also handle
|
||||
// scenarios where there is no existing texture and we can modify
|
||||
// the self pointer directly.
|
||||
const existing: Texture = switch (self.*) {
|
||||
// For pending, we can free the old data and become pending ourselves.
|
||||
.pending_gray => |p| {
|
||||
alloc.free(p.dataSlice(1));
|
||||
self.* = img;
|
||||
return;
|
||||
},
|
||||
|
||||
.pending_gray_alpha => |p| {
|
||||
alloc.free(p.dataSlice(2));
|
||||
self.* = img;
|
||||
return;
|
||||
},
|
||||
|
||||
.pending_rgb => |p| {
|
||||
alloc.free(p.dataSlice(3));
|
||||
self.* = img;
|
||||
return;
|
||||
},
|
||||
|
||||
.pending_rgba => |p| {
|
||||
alloc.free(p.dataSlice(4));
|
||||
self.* = img;
|
||||
return;
|
||||
},
|
||||
|
||||
// If we're marked for unload but we just have pending data,
|
||||
// this behaves the same as a normal "pending": free the data,
|
||||
// become new pending.
|
||||
.unload_pending => |data| {
|
||||
alloc.free(data);
|
||||
self.* = img;
|
||||
return;
|
||||
},
|
||||
|
||||
.unload_replace => |r| existing: {
|
||||
alloc.free(r[0]);
|
||||
break :existing r[1];
|
||||
},
|
||||
|
||||
// If we were already pending a replacement, then we free our
|
||||
// existing pending data and use the same texture.
|
||||
.replace_gray => |r| existing: {
|
||||
alloc.free(r.pending.dataSlice(1));
|
||||
break :existing r.texture;
|
||||
},
|
||||
|
||||
.replace_gray_alpha => |r| existing: {
|
||||
alloc.free(r.pending.dataSlice(2));
|
||||
break :existing r.texture;
|
||||
},
|
||||
|
||||
.replace_rgb => |r| existing: {
|
||||
alloc.free(r.pending.dataSlice(3));
|
||||
break :existing r.texture;
|
||||
},
|
||||
|
||||
.replace_rgba => |r| existing: {
|
||||
alloc.free(r.pending.dataSlice(4));
|
||||
break :existing r.texture;
|
||||
},
|
||||
|
||||
// For both ready and unload_ready, we need to replace the
|
||||
// texture. We can't do that here, so we just mark ourselves
|
||||
// for replacement.
|
||||
.ready, .unload_ready => |tex| tex,
|
||||
};
|
||||
|
||||
// We now have an existing texture, so set the proper replace key.
|
||||
self.* = switch (img) {
|
||||
.pending_gray => |p| .{ .replace_gray = .{
|
||||
.texture = existing,
|
||||
.pending = p,
|
||||
} },
|
||||
|
||||
.pending_gray_alpha => |p| .{ .replace_gray_alpha = .{
|
||||
.texture = existing,
|
||||
.pending = p,
|
||||
} },
|
||||
|
||||
.pending_rgb => |p| .{ .replace_rgb = .{
|
||||
.texture = existing,
|
||||
.pending = p,
|
||||
} },
|
||||
|
||||
.pending_rgba => |p| .{ .replace_rgba = .{
|
||||
.texture = existing,
|
||||
.pending = p,
|
||||
} },
|
||||
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this image is pending upload.
|
||||
pub fn isPending(self: Image) bool {
|
||||
return self.pending() != null;
|
||||
}
|
||||
|
||||
/// Returns true if this image is pending an unload.
|
||||
pub fn isUnloading(self: Image) bool {
|
||||
return switch (self) {
|
||||
.unload_pending,
|
||||
.unload_ready,
|
||||
=> true,
|
||||
|
||||
.ready,
|
||||
.pending_gray,
|
||||
.pending_gray_alpha,
|
||||
.pending_rgb,
|
||||
.pending_rgba,
|
||||
=> false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts the image data to a format that can be uploaded to the GPU.
|
||||
/// If the data is already in a format that can be uploaded, this is a
|
||||
/// no-op.
|
||||
pub fn convert(self: *Image, alloc: Allocator) !void {
|
||||
switch (self.*) {
|
||||
.ready,
|
||||
.unload_pending,
|
||||
.unload_replace,
|
||||
.unload_ready,
|
||||
=> unreachable, // invalid
|
||||
|
||||
.pending_rgba,
|
||||
.replace_rgba,
|
||||
=> {}, // ready
|
||||
|
||||
// RGB needs to be converted to RGBA because Metal textures
|
||||
// don't support RGB.
|
||||
.pending_rgb => |*p| {
|
||||
const data = p.dataSlice(3);
|
||||
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
|
||||
alloc.free(data);
|
||||
p.data = rgba.ptr;
|
||||
self.* = .{ .pending_rgba = p.* };
|
||||
},
|
||||
|
||||
.replace_rgb => |*r| {
|
||||
const data = r.pending.dataSlice(3);
|
||||
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
|
||||
alloc.free(data);
|
||||
r.pending.data = rgba.ptr;
|
||||
self.* = .{ .replace_rgba = r.* };
|
||||
},
|
||||
|
||||
// Gray and Gray+Alpha need to be converted to RGBA, too.
|
||||
.pending_gray => |*p| {
|
||||
const data = p.dataSlice(1);
|
||||
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
|
||||
alloc.free(data);
|
||||
p.data = rgba.ptr;
|
||||
self.* = .{ .pending_rgba = p.* };
|
||||
},
|
||||
|
||||
.replace_gray => |*r| {
|
||||
const data = r.pending.dataSlice(2);
|
||||
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
|
||||
alloc.free(data);
|
||||
r.pending.data = rgba.ptr;
|
||||
self.* = .{ .replace_rgba = r.* };
|
||||
},
|
||||
|
||||
.pending_gray_alpha => |*p| {
|
||||
const data = p.dataSlice(2);
|
||||
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
|
||||
alloc.free(data);
|
||||
p.data = rgba.ptr;
|
||||
self.* = .{ .pending_rgba = p.* };
|
||||
},
|
||||
|
||||
.replace_gray_alpha => |*r| {
|
||||
const data = r.pending.dataSlice(2);
|
||||
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
|
||||
alloc.free(data);
|
||||
r.pending.data = rgba.ptr;
|
||||
self.* = .{ .replace_rgba = r.* };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload the pending image to the GPU and change the state of this
|
||||
/// image to ready.
|
||||
pub fn upload(
|
||||
self: *Image,
|
||||
alloc: Allocator,
|
||||
opengl: *const OpenGL,
|
||||
) !void {
|
||||
_ = opengl;
|
||||
|
||||
// Convert our data if we have to
|
||||
try self.convert(alloc);
|
||||
|
||||
// Get our pending info
|
||||
const p = self.pending().?;
|
||||
|
||||
// Get our format
|
||||
const formats: struct {
|
||||
internal: gl.Texture.InternalFormat,
|
||||
format: gl.Texture.Format,
|
||||
} = switch (self.*) {
|
||||
.pending_rgb, .replace_rgb => .{ .internal = .srgb, .format = .rgb },
|
||||
.pending_rgba, .replace_rgba => .{ .internal = .srgba, .format = .rgba },
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// Create our texture
|
||||
const tex = try Texture.init(
|
||||
.{
|
||||
.format = formats.format,
|
||||
.internal_format = formats.internal,
|
||||
.target = .Rectangle,
|
||||
},
|
||||
@intCast(p.width),
|
||||
@intCast(p.height),
|
||||
p.data[0 .. p.width * p.height * self.depth()],
|
||||
);
|
||||
|
||||
// Uploaded. We can now clear our data and change our state.
|
||||
self.deinit(alloc);
|
||||
self.* = .{ .ready = tex };
|
||||
}
|
||||
|
||||
/// Our pixel depth
|
||||
fn depth(self: Image) u32 {
|
||||
return switch (self) {
|
||||
.pending_rgb => 3,
|
||||
.pending_rgba => 4,
|
||||
.replace_rgb => 3,
|
||||
.replace_rgba => 4,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this image is in a pending state and requires upload.
|
||||
fn pending(self: Image) ?Pending {
|
||||
return switch (self) {
|
||||
.pending_rgb,
|
||||
.pending_rgba,
|
||||
=> |p| p,
|
||||
|
||||
.replace_rgb,
|
||||
.replace_rgba,
|
||||
=> |r| r.pending,
|
||||
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
@@ -7,18 +7,77 @@ const Pipeline = @import("Pipeline.zig");
|
||||
|
||||
const log = std.log.scoped(.opengl);
|
||||
|
||||
const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
|
||||
&.{
|
||||
.{ "bg_color", .{
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/bg_color.f.glsl"),
|
||||
.blending_enabled = false,
|
||||
} },
|
||||
.{ "cell_bg", .{
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
|
||||
.blending_enabled = true,
|
||||
} },
|
||||
.{ "cell_text", .{
|
||||
.vertex_attributes = CellText,
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
|
||||
.step_fn = .per_instance,
|
||||
.blending_enabled = true,
|
||||
} },
|
||||
.{ "image", .{
|
||||
.vertex_attributes = Image,
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
|
||||
.step_fn = .per_instance,
|
||||
.blending_enabled = true,
|
||||
} },
|
||||
};
|
||||
|
||||
/// All the comptime-known info about a pipeline, so that
|
||||
/// we can define them ahead-of-time in an ergonomic way.
|
||||
const PipelineDescription = struct {
|
||||
vertex_attributes: ?type = null,
|
||||
vertex_fn: [:0]const u8,
|
||||
fragment_fn: [:0]const u8,
|
||||
step_fn: Pipeline.Options.StepFunction = .per_vertex,
|
||||
blending_enabled: bool = true,
|
||||
|
||||
fn initPipeline(self: PipelineDescription) !Pipeline {
|
||||
return try .init(self.vertex_attributes, .{
|
||||
.vertex_fn = self.vertex_fn,
|
||||
.fragment_fn = self.fragment_fn,
|
||||
.step_fn = self.step_fn,
|
||||
.blending_enabled = self.blending_enabled,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/// We create a type for the pipeline collection based on our desc array.
|
||||
const PipelineCollection = t: {
|
||||
var fields: [pipeline_descs.len]std.builtin.Type.StructField = undefined;
|
||||
for (pipeline_descs, 0..) |pipeline, i| {
|
||||
fields[i] = .{
|
||||
.name = pipeline[0],
|
||||
.type = Pipeline,
|
||||
.default_value_ptr = null,
|
||||
.is_comptime = false,
|
||||
.alignment = @alignOf(Pipeline),
|
||||
};
|
||||
}
|
||||
break :t @Type(.{ .@"struct" = .{
|
||||
.layout = .auto,
|
||||
.fields = &fields,
|
||||
.decls = &.{},
|
||||
.is_tuple = false,
|
||||
} });
|
||||
};
|
||||
|
||||
/// This contains the state for the shaders used by the Metal renderer.
|
||||
pub const Shaders = struct {
|
||||
/// Renders cell foreground elements (text, decorations).
|
||||
cell_text_pipeline: Pipeline,
|
||||
|
||||
/// The cell background shader is the shader used to render the
|
||||
/// background of terminal cells.
|
||||
cell_bg_pipeline: Pipeline,
|
||||
|
||||
/// The image shader is the shader used to render images for things
|
||||
/// like the Kitty image protocol.
|
||||
image_pipeline: Pipeline,
|
||||
/// Collection of available render pipelines.
|
||||
pipelines: PipelineCollection,
|
||||
|
||||
/// Custom shaders to run against the final drawable texture. This
|
||||
/// can be used to apply a lot of effects. Each shader is run in sequence
|
||||
@@ -38,14 +97,20 @@ pub const Shaders = struct {
|
||||
alloc: Allocator,
|
||||
post_shaders: []const [:0]const u8,
|
||||
) !Shaders {
|
||||
const cell_text_pipeline = try initCellTextPipeline();
|
||||
errdefer cell_text_pipeline.deinit();
|
||||
var pipelines: PipelineCollection = undefined;
|
||||
|
||||
const cell_bg_pipeline = try initCellBgPipeline();
|
||||
errdefer cell_bg_pipeline.deinit();
|
||||
var initialized_pipelines: usize = 0;
|
||||
|
||||
const image_pipeline = try initImagePipeline();
|
||||
errdefer image_pipeline.deinit();
|
||||
errdefer inline for (pipeline_descs, 0..) |pipeline, i| {
|
||||
if (i < initialized_pipelines) {
|
||||
@field(pipelines, pipeline[0]).deinit();
|
||||
}
|
||||
};
|
||||
|
||||
inline for (pipeline_descs) |pipeline| {
|
||||
@field(pipelines, pipeline[0]) = try pipeline[1].initPipeline();
|
||||
initialized_pipelines += 1;
|
||||
}
|
||||
|
||||
const post_pipelines: []const Pipeline = initPostPipelines(
|
||||
alloc,
|
||||
@@ -63,9 +128,7 @@ pub const Shaders = struct {
|
||||
};
|
||||
|
||||
return .{
|
||||
.cell_text_pipeline = cell_text_pipeline,
|
||||
.cell_bg_pipeline = cell_bg_pipeline,
|
||||
.image_pipeline = image_pipeline,
|
||||
.pipelines = pipelines,
|
||||
.post_pipelines = post_pipelines,
|
||||
};
|
||||
}
|
||||
@@ -75,9 +138,9 @@ pub const Shaders = struct {
|
||||
self.defunct = true;
|
||||
|
||||
// Release our primary shaders
|
||||
self.cell_text_pipeline.deinit();
|
||||
self.cell_bg_pipeline.deinit();
|
||||
self.image_pipeline.deinit();
|
||||
inline for (pipeline_descs) |pipeline| {
|
||||
@field(self.pipelines, pipeline[0]).deinit();
|
||||
}
|
||||
|
||||
// Release our postprocess shaders
|
||||
if (self.post_pipelines.len > 0) {
|
||||
@@ -89,15 +152,7 @@ pub const Shaders = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
pub const Image = extern struct {
|
||||
grid_pos: [2]f32 align(8),
|
||||
cell_offset: [2]f32 align(8),
|
||||
source_rect: [4]f32 align(16),
|
||||
dest_size: [2]f32 align(8),
|
||||
};
|
||||
|
||||
/// The uniforms that are passed to the terminal cell shader.
|
||||
/// The uniforms that are passed to our shaders.
|
||||
pub const Uniforms = extern struct {
|
||||
/// The projection matrix for turning world coordinates to normalized.
|
||||
/// This is calculated based on the size of the screen.
|
||||
@@ -165,6 +220,42 @@ pub const Uniforms = extern struct {
|
||||
};
|
||||
};
|
||||
|
||||
/// This is a single parameter for the terminal cell shader.
|
||||
pub const CellText = extern struct {
|
||||
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
|
||||
glyph_size: [2]u32 align(8) = .{ 0, 0 },
|
||||
bearings: [2]i16 align(4) = .{ 0, 0 },
|
||||
grid_pos: [2]u16 align(4),
|
||||
color: [4]u8 align(4),
|
||||
mode: Mode align(4),
|
||||
constraint_width: u32 align(4) = 0,
|
||||
|
||||
pub const Mode = enum(u32) {
|
||||
fg = 1,
|
||||
fg_constrained = 2,
|
||||
fg_color = 3,
|
||||
cursor = 4,
|
||||
fg_powerline = 5,
|
||||
};
|
||||
|
||||
// test {
|
||||
// // Minimizing the size of this struct is important,
|
||||
// // so we test it in order to be aware of any changes.
|
||||
// try std.testing.expectEqual(32, @sizeOf(CellText));
|
||||
// }
|
||||
};
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub const CellBg = [4]u8;
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
pub const Image = extern struct {
|
||||
grid_pos: [2]f32 align(8),
|
||||
cell_offset: [2]f32 align(8),
|
||||
source_rect: [4]f32 align(16),
|
||||
dest_size: [2]f32 align(8),
|
||||
};
|
||||
|
||||
/// Initialize our custom shader pipelines. The shaders argument is a
|
||||
/// set of shader source code, not file paths.
|
||||
fn initPostPipelines(
|
||||
@@ -204,60 +295,6 @@ fn initPostPipeline(data: [:0]const u8) !Pipeline {
|
||||
});
|
||||
}
|
||||
|
||||
/// This is a single parameter for the terminal cell shader.
|
||||
pub const CellText = extern struct {
|
||||
glyph_pos: [2]u32 align(8) = .{ 0, 0 },
|
||||
glyph_size: [2]u32 align(8) = .{ 0, 0 },
|
||||
bearings: [2]i16 align(4) = .{ 0, 0 },
|
||||
grid_pos: [2]u16 align(4),
|
||||
color: [4]u8 align(4),
|
||||
mode: Mode align(4),
|
||||
constraint_width: u32 align(4) = 0,
|
||||
|
||||
pub const Mode = enum(u32) {
|
||||
fg = 1,
|
||||
fg_constrained = 2,
|
||||
fg_color = 3,
|
||||
cursor = 4,
|
||||
fg_powerline = 5,
|
||||
};
|
||||
|
||||
// test {
|
||||
// // Minimizing the size of this struct is important,
|
||||
// // so we test it in order to be aware of any changes.
|
||||
// try std.testing.expectEqual(32, @sizeOf(CellText));
|
||||
// }
|
||||
};
|
||||
|
||||
/// Initialize the cell render pipeline.
|
||||
fn initCellTextPipeline() !Pipeline {
|
||||
return try Pipeline.init(CellText, .{
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
|
||||
.step_fn = .per_instance,
|
||||
});
|
||||
}
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub const CellBg = [4]u8;
|
||||
|
||||
/// Initialize the cell background render pipeline.
|
||||
fn initCellBgPipeline() !Pipeline {
|
||||
return try Pipeline.init(null, .{
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize the image render pipeline.
|
||||
fn initImagePipeline() !Pipeline {
|
||||
return try Pipeline.init(Image, .{
|
||||
.vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
|
||||
.fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
|
||||
.step_fn = .per_instance,
|
||||
});
|
||||
}
|
||||
|
||||
/// Load shader code from the target path, processing `#include` directives.
|
||||
///
|
||||
/// Comptime only for now, this code is really sloppy and makes a bunch of
|
||||
|
13
src/renderer/shaders/glsl/bg_color.f.glsl
Normal file
13
src/renderer/shaders/glsl/bg_color.f.glsl
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "common.glsl"
|
||||
|
||||
// Must declare this output for some versions of OpenGL.
|
||||
layout(location = 0) out vec4 out_FragColor;
|
||||
|
||||
void main() {
|
||||
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||
|
||||
out_FragColor = load_color(
|
||||
unpack4u8(bg_color_packed_4u8),
|
||||
use_linear_blending
|
||||
);
|
||||
}
|
@@ -15,7 +15,7 @@ vec4 cell_bg() {
|
||||
ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
|
||||
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
|
||||
|
||||
vec4 bg = load_color(unpack4u8(bg_color_packed_4u8), use_linear_blending);
|
||||
vec4 bg = vec4(0.0);
|
||||
|
||||
// Clamp x position, extends edge bg colors in to padding on sides.
|
||||
if (grid_pos.x < 0) {
|
||||
|
@@ -87,19 +87,19 @@ void main() {
|
||||
case MODE_TEXT_COLOR:
|
||||
{
|
||||
// For now, we assume that color glyphs
|
||||
// are already premultiplied sRGB colors.
|
||||
// are already premultiplied linear colors.
|
||||
vec4 color = texture(atlas_color, in_data.tex_coord);
|
||||
|
||||
// If we aren't doing linear blending, we can return this right away.
|
||||
if (!use_linear_blending) {
|
||||
// If we are doing linear blending, we can return this right away.
|
||||
if (use_linear_blending) {
|
||||
out_FragColor = color;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise we need to linearize the color. Since the alpha is
|
||||
// premultiplied, we need to divide it out before linearizing.
|
||||
// Otherwise we need to unlinearize the color. Since the alpha is
|
||||
// premultiplied, we need to divide it out before unlinearizing.
|
||||
color.rgb /= vec3(color.a);
|
||||
color = linearize(color);
|
||||
color = unlinearize(color);
|
||||
color.rgb *= vec3(color.a);
|
||||
|
||||
out_FragColor = color;
|
||||
|
@@ -139,6 +139,12 @@ void main() {
|
||||
unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
|
||||
true
|
||||
);
|
||||
// Blend it with the global bg color
|
||||
vec4 global_bg = load_color(
|
||||
unpack4u8(bg_color_packed_4u8),
|
||||
true
|
||||
);
|
||||
out_data.bg_color += global_bg * vec4(1.0 - out_data.bg_color.a);
|
||||
|
||||
// If we have a minimum contrast, we need to check if we need to
|
||||
// change the color of the text to ensure it has enough contrast
|
||||
|
@@ -216,45 +216,34 @@ vertex FullScreenVertexOut full_screen_vertex(
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Cell Background Shader
|
||||
// Background Color Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Cell BG Shader
|
||||
#pragma mark - BG Color Shader
|
||||
|
||||
struct CellBgVertexOut {
|
||||
float4 position [[position]];
|
||||
float4 bg_color;
|
||||
};
|
||||
|
||||
vertex CellBgVertexOut cell_bg_vertex(
|
||||
uint vid [[vertex_id]],
|
||||
fragment float4 bg_color_fragment(
|
||||
FullScreenVertexOut in [[stage_in]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
CellBgVertexOut out;
|
||||
|
||||
float4 position;
|
||||
position.x = (vid == 2) ? 3.0 : -1.0;
|
||||
position.y = (vid == 0) ? -3.0 : 1.0;
|
||||
position.zw = 1.0;
|
||||
out.position = position;
|
||||
|
||||
// Convert the background color to Display P3
|
||||
out.bg_color = load_color(
|
||||
return load_color(
|
||||
uniforms.bg_color,
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Cell Background Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Cell BG Shader
|
||||
|
||||
fragment float4 cell_bg_fragment(
|
||||
CellBgVertexOut in [[stage_in]],
|
||||
FullScreenVertexOut in [[stage_in]],
|
||||
constant Uniforms& uniforms [[buffer(1)]],
|
||||
constant uchar4 *cells [[buffer(2)]]
|
||||
) {
|
||||
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
|
||||
|
||||
float4 bg = in.bg_color;
|
||||
float4 bg = float4(0.0);
|
||||
|
||||
// Clamp x position, extends edge bg colors in to padding on sides.
|
||||
if (grid_pos.x < 0) {
|
||||
@@ -289,17 +278,8 @@ fragment float4 cell_bg_fragment(
|
||||
// Load the color for the cell.
|
||||
uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
|
||||
|
||||
// We have special case handling for when the cell color matches the bg color.
|
||||
if (all(cell_color == uniforms.bg_color)) {
|
||||
return bg;
|
||||
}
|
||||
|
||||
// Convert the color and return it.
|
||||
//
|
||||
// TODO: We may want to blend the color with the background
|
||||
// color, rather than purely replacing it, this needs
|
||||
// some consideration about config options though.
|
||||
//
|
||||
// TODO: It might be a good idea to do a pass before this
|
||||
// to convert all of the bg colors, so we don't waste
|
||||
// a bunch of work converting the cell color in every
|
||||
@@ -462,6 +442,13 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
// Blend it with the global bg color
|
||||
float4 global_bg = load_color(
|
||||
uniforms.bg_color,
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
out.bg_color += global_bg * (1.0 - out.bg_color.a);
|
||||
|
||||
// If we have a minimum contrast, we need to check if we need to
|
||||
// change the color of the text to ensure it has enough contrast
|
||||
@@ -566,19 +553,19 @@ fragment float4 cell_text_fragment(
|
||||
}
|
||||
|
||||
case MODE_TEXT_COLOR: {
|
||||
// For now, we assume that color glyphs are
|
||||
// already premultiplied Display P3 colors.
|
||||
// For now, we assume that color glyphs
|
||||
// are already premultiplied linear colors.
|
||||
float4 color = textureColor.sample(textureSampler, in.tex_coord);
|
||||
|
||||
// If we aren't doing linear blending, we can return this right away.
|
||||
if (!uniforms.use_linear_blending) {
|
||||
// If we're doing linear blending, we can return this right away.
|
||||
if (uniforms.use_linear_blending) {
|
||||
return color;
|
||||
}
|
||||
|
||||
// Otherwise we need to linearize the color. Since the alpha is
|
||||
// premultiplied, we need to divide it out before linearizing.
|
||||
// Otherwise we need to unlinearize the color. Since the alpha is
|
||||
// premultiplied, we need to divide it out before unlinearizing.
|
||||
color.rgb /= color.a;
|
||||
color = linearize(color);
|
||||
color = unlinearize(color);
|
||||
color.rgb *= color.a;
|
||||
|
||||
return color;
|
Reference in New Issue
Block a user