feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
3c8fae1089
commit
452b782149
124 files changed
+14936
-1242
No files matched your search
@@ -8,6 +8,7 @@ license.workspace = true
|
||||
bytemuck = { version = "1.25.2", features = ["derive"] }
|
||||
crossbeam-channel = "0.5.17"
|
||||
ghostty-config.workspace = true
|
||||
ghostty-crash.workspace = true
|
||||
ghostty-datastruct.workspace = true
|
||||
ghostty-font.workspace = true
|
||||
ghostty-input.workspace = true
|
||||
@@ -18,7 +19,7 @@ glow = "0.18.0"
|
||||
jpeg-decoder = { version = "0.3.2", default-features = false }
|
||||
khronos-egl = { version = "6.0.0", features = ["dynamic"] }
|
||||
log = "0.4.34"
|
||||
naga = { version = "30.0.1", features = ["glsl-in", "glsl-out", "wgsl-in"] }
|
||||
naga = { version = "30.0.1", features = ["glsl-in", "glsl-out", "msl-out", "wgsl-in"] }
|
||||
onig = "6.5.3"
|
||||
png = "0.18.1"
|
||||
thiserror = "2.0.21"
|
||||
@@ -54,3 +55,21 @@ return_self_not_must_use = "allow"
|
||||
used_underscore_binding = "allow"
|
||||
large_stack_arrays = "allow"
|
||||
inline_always = "allow"
|
||||
|
||||
# The Metal graphics API (`renderer/Metal.zig`) and the CoreVideo display
|
||||
# link; compiled on macOS and iOS only.
|
||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||
block2 = "0.6.2"
|
||||
dispatch2 = "0.3.1"
|
||||
ghostty-os.workspace = true
|
||||
objc2 = "0.6.5"
|
||||
objc2-core-foundation = "0.3.2"
|
||||
objc2-core-graphics = "0.3.2"
|
||||
objc2-foundation = "0.3.2"
|
||||
objc2-io-surface = "0.3.2"
|
||||
objc2-metal = { version = "0.3.2", features = ["block2", "objc2-io-surface"] }
|
||||
objc2-quartz-core = "0.3.2"
|
||||
|
||||
# CVDisplayLink does not exist on iOS.
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2-core-video = "0.3.2"
|
||||
@@ -0,0 +1,853 @@
|
||||
#include <metal_stdlib>
|
||||
|
||||
using namespace metal;
|
||||
|
||||
enum Padding : uint8_t {
|
||||
EXTEND_LEFT = 1u,
|
||||
EXTEND_RIGHT = 2u,
|
||||
EXTEND_UP = 4u,
|
||||
EXTEND_DOWN = 8u,
|
||||
};
|
||||
|
||||
struct Uniforms {
|
||||
float4x4 projection_matrix;
|
||||
float2 screen_size;
|
||||
float2 cell_size;
|
||||
ushort2 grid_size;
|
||||
float4 grid_padding;
|
||||
uint8_t padding_extend;
|
||||
float min_contrast;
|
||||
ushort2 cursor_pos;
|
||||
uchar4 cursor_color;
|
||||
uchar4 bg_color;
|
||||
bool cursor_wide;
|
||||
bool use_display_p3;
|
||||
bool use_linear_blending;
|
||||
bool use_linear_correction;
|
||||
};
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Color Functions
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Colors
|
||||
|
||||
// D50-adapted sRGB to XYZ conversion matrix.
|
||||
// http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html
|
||||
constant float3x3 sRGB_XYZ = transpose(float3x3(
|
||||
0.4360747, 0.3850649, 0.1430804,
|
||||
0.2225045, 0.7168786, 0.0606169,
|
||||
0.0139322, 0.0971045, 0.7141733
|
||||
));
|
||||
// XYZ to Display P3 conversion matrix.
|
||||
// http://endavid.com/index.php?entry=79
|
||||
constant float3x3 XYZ_DP3 = transpose(float3x3(
|
||||
2.40414768,-0.99010704,-0.39759019,
|
||||
-0.84239098, 1.79905954, 0.01597023,
|
||||
0.04838763,-0.09752546, 1.27393636
|
||||
));
|
||||
// By composing the two above matrices we get
|
||||
// our sRGB to Display P3 conversion matrix.
|
||||
constant float3x3 sRGB_DP3 = XYZ_DP3 * sRGB_XYZ;
|
||||
|
||||
// Converts a color in linear sRGB to linear Display P3
|
||||
//
|
||||
// TODO: The color matrix should probably be computed
|
||||
// dynamically and passed as a uniform, rather
|
||||
// than being hard coded above.
|
||||
float3 srgb_to_display_p3(float3 srgb) {
|
||||
return sRGB_DP3 * srgb;
|
||||
}
|
||||
|
||||
// Converts a color from sRGB gamma encoding to linear.
|
||||
float4 linearize(float4 srgb) {
|
||||
bool3 cutoff = srgb.rgb <= 0.04045;
|
||||
float3 lower = srgb.rgb / 12.92;
|
||||
float3 higher = pow((srgb.rgb + 0.055) / 1.055, 2.4);
|
||||
srgb.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return srgb;
|
||||
}
|
||||
float linearize(float v) {
|
||||
return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
// Converts a color from linear to sRGB gamma encoding.
|
||||
float4 unlinearize(float4 linear) {
|
||||
bool3 cutoff = linear.rgb <= 0.0031308;
|
||||
float3 lower = linear.rgb * 12.92;
|
||||
float3 higher = pow(linear.rgb, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
linear.rgb = mix(higher, lower, float3(cutoff));
|
||||
|
||||
return linear;
|
||||
}
|
||||
float unlinearize(float v) {
|
||||
return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
|
||||
}
|
||||
|
||||
// Compute the luminance of the provided color.
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float luminance(float3 color) {
|
||||
return dot(color, float3(0.2126f, 0.7152f, 0.0722f));
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float contrast_ratio(float3 color1, float3 color2) {
|
||||
float l1 = luminance(color1);
|
||||
float l2 = luminance(color2);
|
||||
return (max(l1, l2) + 0.05f) / (min(l1, l2) + 0.05f);
|
||||
}
|
||||
|
||||
// Return the fg if the contrast ratio is greater than min, otherwise
|
||||
// return a color that satisfies the contrast ratio. Currently, the color
|
||||
// is always white or black, whichever has the highest contrast ratio.
|
||||
//
|
||||
// Takes colors in linear RGB space. If your colors are gamma
|
||||
// encoded, linearize them before using them with this function.
|
||||
float4 contrasted_color(float min, float4 fg, float4 bg) {
|
||||
float ratio = contrast_ratio(fg.rgb, bg.rgb);
|
||||
if (ratio < min) {
|
||||
float white_ratio = contrast_ratio(float3(1.0f), bg.rgb);
|
||||
float black_ratio = contrast_ratio(float3(0.0f), bg.rgb);
|
||||
if (white_ratio > black_ratio) {
|
||||
return float4(1.0f);
|
||||
} else {
|
||||
return float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
return fg;
|
||||
}
|
||||
|
||||
// Load a 4 byte RGBA non-premultiplied color and linearize
|
||||
// and convert it as necessary depending on the provided info.
|
||||
//
|
||||
// Returns a color in the Display P3 color space.
|
||||
//
|
||||
// If `display_p3` is true, then the provided color is assumed to
|
||||
// already be in the Display P3 color space, otherwise it's treated
|
||||
// as an sRGB color and is appropriately converted to Display P3.
|
||||
//
|
||||
// `linear` controls whether the returned color is linear or gamma encoded.
|
||||
float4 load_color(
|
||||
uchar4 in_color,
|
||||
bool display_p3,
|
||||
bool linear
|
||||
) {
|
||||
// 0 .. 255 -> 0.0 .. 1.0
|
||||
float4 color = float4(in_color) / 255.0f;
|
||||
|
||||
// If our color is already in Display P3 and
|
||||
// we aren't doing linear blending, then we
|
||||
// already have the correct color here and
|
||||
// can premultiply and return it.
|
||||
if (display_p3 && !linear) {
|
||||
color.rgb *= color.a;
|
||||
return color;
|
||||
}
|
||||
|
||||
// The color is in either the sRGB or Display P3 color space,
|
||||
// so in either case, it's a color space which uses the sRGB
|
||||
// transfer function, so we can use one function in order to
|
||||
// linearize it in either case.
|
||||
//
|
||||
// Even if we aren't doing linear blending, the color
|
||||
// needs to be in linear space to convert color spaces.
|
||||
color = linearize(color);
|
||||
|
||||
// If we're *NOT* using display P3 colors, then we're dealing
|
||||
// with an sRGB color, in which case we need to convert it in
|
||||
// to the Display P3 color space, since our output is always
|
||||
// Display P3.
|
||||
if (!display_p3) {
|
||||
color.rgb = srgb_to_display_p3(color.rgb);
|
||||
}
|
||||
|
||||
// If we're not doing linear blending, then we need to
|
||||
// unlinearize after doing the color space conversion.
|
||||
if (!linear) {
|
||||
color = unlinearize(color);
|
||||
}
|
||||
|
||||
// Premultiply our color by its alpha.
|
||||
color.rgb *= color.a;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Full Screen Vertex Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Full Screen Vertex Shader
|
||||
|
||||
struct FullScreenVertexOut {
|
||||
float4 position [[position]];
|
||||
};
|
||||
|
||||
vertex FullScreenVertexOut full_screen_vertex(
|
||||
uint vid [[vertex_id]]
|
||||
) {
|
||||
FullScreenVertexOut out;
|
||||
|
||||
float4 position;
|
||||
position.x = (vid == 2) ? 3.0 : -1.0;
|
||||
position.y = (vid == 0) ? -3.0 : 1.0;
|
||||
position.zw = 1.0;
|
||||
|
||||
// Single triangle is clipped to viewport.
|
||||
//
|
||||
// X <- vid == 0: (-1, -3)
|
||||
// |\
|
||||
// | \
|
||||
// | \
|
||||
// |###\
|
||||
// |#+# \ `+` is (0, 0). `#`s are viewport area.
|
||||
// |### \
|
||||
// X------X <- vid == 2: (3, 1)
|
||||
// ^
|
||||
// vid == 1: (-1, 1)
|
||||
|
||||
out.position = position;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Background Color Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - BG Color Shader
|
||||
|
||||
fragment float4 bg_color_fragment(
|
||||
FullScreenVertexOut in [[stage_in]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
return load_color(
|
||||
uniforms.bg_color,
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Background Image Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - BG Image Shader
|
||||
|
||||
struct BgImageVertexIn {
|
||||
float opacity [[attribute(0)]];
|
||||
uint8_t info [[attribute(1)]];
|
||||
};
|
||||
|
||||
enum BgImagePosition : uint8_t {
|
||||
// 4 bits of info.
|
||||
BG_IMAGE_POSITION = 15u,
|
||||
|
||||
BG_IMAGE_TL = 0u,
|
||||
BG_IMAGE_TC = 1u,
|
||||
BG_IMAGE_TR = 2u,
|
||||
BG_IMAGE_ML = 3u,
|
||||
BG_IMAGE_MC = 4u,
|
||||
BG_IMAGE_MR = 5u,
|
||||
BG_IMAGE_BL = 6u,
|
||||
BG_IMAGE_BC = 7u,
|
||||
BG_IMAGE_BR = 8u,
|
||||
};
|
||||
|
||||
enum BgImageFit : uint8_t {
|
||||
// 2 bits of info shifted 4.
|
||||
BG_IMAGE_FIT = 3u << 4,
|
||||
|
||||
BG_IMAGE_CONTAIN = 0u << 4,
|
||||
BG_IMAGE_COVER = 1u << 4,
|
||||
BG_IMAGE_STRETCH = 2u << 4,
|
||||
BG_IMAGE_NO_FIT = 3u << 4,
|
||||
};
|
||||
|
||||
enum BgImageRepeat : uint8_t {
|
||||
// 1 bit of info shifted 6.
|
||||
BG_IMAGE_REPEAT = 1u << 6,
|
||||
};
|
||||
|
||||
struct BgImageVertexOut {
|
||||
float4 position [[position]];
|
||||
float4 bg_color [[flat]];
|
||||
float2 offset [[flat]];
|
||||
float2 scale [[flat]];
|
||||
float opacity [[flat]];
|
||||
bool repeat [[flat]];
|
||||
};
|
||||
|
||||
vertex BgImageVertexOut bg_image_vertex(
|
||||
uint vid [[vertex_id]],
|
||||
BgImageVertexIn in [[stage_in]],
|
||||
texture2d<float> image [[texture(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
BgImageVertexOut out;
|
||||
|
||||
float4 position;
|
||||
position.x = (vid == 2) ? 3.0 : -1.0;
|
||||
position.y = (vid == 0) ? -3.0 : 1.0;
|
||||
position.zw = 1.0;
|
||||
|
||||
// Single triangle is clipped to viewport.
|
||||
//
|
||||
// X <- vid == 0: (-1, -3)
|
||||
// |\
|
||||
// | \
|
||||
// | \
|
||||
// |###\
|
||||
// |#+# \ `+` is (0, 0). `#`s are viewport area.
|
||||
// |### \
|
||||
// X------X <- vid == 2: (3, 1)
|
||||
// ^
|
||||
// vid == 1: (-1, 1)
|
||||
|
||||
out.position = position;
|
||||
|
||||
out.opacity = in.opacity;
|
||||
|
||||
out.repeat = (in.info & BG_IMAGE_REPEAT) == BG_IMAGE_REPEAT;
|
||||
|
||||
float2 screen_size = uniforms.screen_size;
|
||||
float2 tex_size = float2(image.get_width(), image.get_height());
|
||||
|
||||
float2 dest_size = tex_size;
|
||||
switch (in.info & BG_IMAGE_FIT) {
|
||||
// For `contain` we scale by a factor that makes the image
|
||||
// width match the screen width or makes the image height
|
||||
// match the screen height, whichever is smaller.
|
||||
case BG_IMAGE_CONTAIN: {
|
||||
float scale = min(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
|
||||
dest_size = tex_size * scale;
|
||||
} break;
|
||||
|
||||
// For `cover` we scale by a factor that makes the image
|
||||
// width match the screen width or makes the image height
|
||||
// match the screen height, whichever is larger.
|
||||
case BG_IMAGE_COVER: {
|
||||
float scale = max(screen_size.x / tex_size.x, screen_size.y / tex_size.y);
|
||||
dest_size = tex_size * scale;
|
||||
} break;
|
||||
|
||||
// For `stretch` we stretch the image to the size of
|
||||
// the screen without worrying about aspect ratio.
|
||||
case BG_IMAGE_STRETCH: {
|
||||
dest_size = screen_size;
|
||||
} break;
|
||||
|
||||
// For `none` we just use the original texture size.
|
||||
case BG_IMAGE_NO_FIT: {
|
||||
dest_size = tex_size;
|
||||
} break;
|
||||
}
|
||||
|
||||
float2 start = float2(0.0);
|
||||
float2 mid = (screen_size - dest_size) / 2;
|
||||
float2 end = screen_size - dest_size;
|
||||
|
||||
float2 dest_offset = mid;
|
||||
switch (in.info & BG_IMAGE_POSITION) {
|
||||
case BG_IMAGE_TL: {
|
||||
dest_offset = float2(start.x, start.y);
|
||||
} break;
|
||||
case BG_IMAGE_TC: {
|
||||
dest_offset = float2(mid.x, start.y);
|
||||
} break;
|
||||
case BG_IMAGE_TR: {
|
||||
dest_offset = float2(end.x, start.y);
|
||||
} break;
|
||||
case BG_IMAGE_ML: {
|
||||
dest_offset = float2(start.x, mid.y);
|
||||
} break;
|
||||
case BG_IMAGE_MC: {
|
||||
dest_offset = float2(mid.x, mid.y);
|
||||
} break;
|
||||
case BG_IMAGE_MR: {
|
||||
dest_offset = float2(end.x, mid.y);
|
||||
} break;
|
||||
case BG_IMAGE_BL: {
|
||||
dest_offset = float2(start.x, end.y);
|
||||
} break;
|
||||
case BG_IMAGE_BC: {
|
||||
dest_offset = float2(mid.x, end.y);
|
||||
} break;
|
||||
case BG_IMAGE_BR: {
|
||||
dest_offset = float2(end.x, end.y);
|
||||
} break;
|
||||
}
|
||||
|
||||
out.offset = dest_offset;
|
||||
out.scale = tex_size / dest_size;
|
||||
|
||||
// We load a fully opaque version of the bg color and combine it with
|
||||
// the alpha separately, because we need these as separate values in
|
||||
// the framgment shader.
|
||||
out.bg_color = float4(load_color(
|
||||
uchar4(uniforms.bg_color.rgb, 255),
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
).rgb, float(uniforms.bg_color.a) / 255.0);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 bg_image_fragment(
|
||||
BgImageVertexOut in [[stage_in]],
|
||||
texture2d<float> image [[texture(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(
|
||||
coord::pixel,
|
||||
address::clamp_to_zero,
|
||||
filter::linear
|
||||
);
|
||||
|
||||
// Our texture coordinate is based on the screen position, offset by the
|
||||
// dest rect origin, and scaled by the ratio between the dest rect size
|
||||
// and the original texture size, which effectively scales the original
|
||||
// size of the texture to the dest rect size.
|
||||
float2 tex_coord = (in.position.xy - in.offset) * in.scale;
|
||||
|
||||
// If we need to repeat the texture, wrap the coordinates.
|
||||
if (in.repeat) {
|
||||
float2 tex_size = float2(image.get_width(), image.get_height());
|
||||
|
||||
tex_coord = fmod(fmod(tex_coord, tex_size) + tex_size, tex_size);
|
||||
}
|
||||
|
||||
float4 rgba = image.sample(textureSampler, tex_coord);
|
||||
|
||||
if (!uniforms.use_linear_blending) {
|
||||
rgba = unlinearize(rgba);
|
||||
}
|
||||
|
||||
// Premultiply the bg image.
|
||||
rgba.rgb *= rgba.a;
|
||||
|
||||
// Multiply it by the configured opacity, but cap it at
|
||||
// the value that will make it fully opaque relative to
|
||||
// the background color alpha, so it isn't overexposed.
|
||||
rgba *= min(in.opacity, 1.0 / in.bg_color.a);
|
||||
|
||||
// Blend it on to a fully opaque version of the background color.
|
||||
rgba += max(float4(0.0), float4(in.bg_color.rgb, 1.0) * (1.0 - rgba.a));
|
||||
|
||||
// Multiply everything by the background color alpha.
|
||||
rgba *= in.bg_color.a;
|
||||
|
||||
return rgba;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Cell Background Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Cell BG Shader
|
||||
|
||||
fragment float4 cell_bg_fragment(
|
||||
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 = float4(0.0);
|
||||
|
||||
// Clamp x position, extends edge bg colors in to padding on sides.
|
||||
if (grid_pos.x < 0) {
|
||||
if (uniforms.padding_extend & EXTEND_LEFT) {
|
||||
grid_pos.x = 0;
|
||||
} else {
|
||||
return bg;
|
||||
}
|
||||
} else if (grid_pos.x > uniforms.grid_size.x - 1) {
|
||||
if (uniforms.padding_extend & EXTEND_RIGHT) {
|
||||
grid_pos.x = uniforms.grid_size.x - 1;
|
||||
} else {
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp y position if we should extend, otherwise discard if out of bounds.
|
||||
if (grid_pos.y < 0) {
|
||||
if (uniforms.padding_extend & EXTEND_UP) {
|
||||
grid_pos.y = 0;
|
||||
} else {
|
||||
return bg;
|
||||
}
|
||||
} else if (grid_pos.y > uniforms.grid_size.y - 1) {
|
||||
if (uniforms.padding_extend & EXTEND_DOWN) {
|
||||
grid_pos.y = uniforms.grid_size.y - 1;
|
||||
} else {
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the color for the cell.
|
||||
uchar4 cell_color = cells[grid_pos.y * uniforms.grid_size.x + grid_pos.x];
|
||||
|
||||
// Convert the color and return it.
|
||||
//
|
||||
// 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
|
||||
// fragment of each cell. It's not the most epxensive
|
||||
// operation, but it is still wasted work.
|
||||
return load_color(
|
||||
cell_color,
|
||||
uniforms.use_display_p3,
|
||||
uniforms.use_linear_blending
|
||||
);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Cell Text Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Cell Text Shader
|
||||
|
||||
enum CellTextAtlas : uint8_t {
|
||||
ATLAS_GRAYSCALE = 0u,
|
||||
ATLAS_COLOR = 1u,
|
||||
};
|
||||
|
||||
// We use a packed struct of bools for misc properties of the glyph.
|
||||
enum CellTextBools : uint8_t {
|
||||
// Don't apply min contrast to this glyph.
|
||||
NO_MIN_CONTRAST = 1u,
|
||||
// This is the cursor glyph.
|
||||
IS_CURSOR_GLYPH = 2u,
|
||||
};
|
||||
|
||||
struct CellTextVertexIn {
|
||||
// The position of the glyph in the texture (x, y)
|
||||
uint2 glyph_pos [[attribute(0)]];
|
||||
|
||||
// The size of the glyph in the texture (w, h)
|
||||
uint2 glyph_size [[attribute(1)]];
|
||||
|
||||
// The left and top bearings for the glyph (x, y)
|
||||
int2 bearings [[attribute(2)]];
|
||||
|
||||
// The grid coordinates (x, y) where x < columns and y < rows
|
||||
ushort2 grid_pos [[attribute(3)]];
|
||||
|
||||
// The color of the rendered text glyph.
|
||||
uchar4 color [[attribute(4)]];
|
||||
|
||||
// Which atlas to sample for our glyph.
|
||||
uint8_t atlas [[attribute(5)]];
|
||||
|
||||
// Misc properties of the glyph.
|
||||
uint8_t bools [[attribute(6)]];
|
||||
};
|
||||
|
||||
struct CellTextVertexOut {
|
||||
float4 position [[position]];
|
||||
uint8_t atlas [[flat]];
|
||||
float4 color [[flat]];
|
||||
float4 bg_color [[flat]];
|
||||
float2 tex_coord;
|
||||
};
|
||||
|
||||
vertex CellTextVertexOut cell_text_vertex(
|
||||
uint vid [[vertex_id]],
|
||||
CellTextVertexIn in [[stage_in]],
|
||||
constant Uniforms& uniforms [[buffer(1)]],
|
||||
constant uchar4 *bg_colors [[buffer(2)]]
|
||||
) {
|
||||
// Convert the grid x, y into world space x, y by accounting for cell size
|
||||
float2 cell_pos = uniforms.cell_size * float2(in.grid_pos);
|
||||
|
||||
// We use a triangle strip with 4 vertices to render quads,
|
||||
// so we determine which corner of the cell this vertex is in
|
||||
// based on the vertex ID.
|
||||
//
|
||||
// 0 --> 1
|
||||
// | .'|
|
||||
// | / |
|
||||
// | L |
|
||||
// 2 --> 3
|
||||
//
|
||||
// 0 = top-left (0, 0)
|
||||
// 1 = top-right (1, 0)
|
||||
// 2 = bot-left (0, 1)
|
||||
// 3 = bot-right (1, 1)
|
||||
float2 corner;
|
||||
corner.x = float(vid == 1 || vid == 3);
|
||||
corner.y = float(vid == 2 || vid == 3);
|
||||
|
||||
CellTextVertexOut out;
|
||||
out.atlas = in.atlas;
|
||||
|
||||
// === Grid Cell ===
|
||||
// +X
|
||||
// 0,0--...->
|
||||
// |
|
||||
// . offset.x = bearings.x
|
||||
// +Y. .|.
|
||||
// . | |
|
||||
// | cell_pos -> +-------+ _.
|
||||
// v ._| |_. _|- offset.y = cell_size.y - bearings.y
|
||||
// | | .###. | |
|
||||
// | | #...# | |
|
||||
// glyph_size.y -+ | ##### | |
|
||||
// | | #.... | +- bearings.y
|
||||
// |_| .#### | |
|
||||
// | |_|
|
||||
// +-------+
|
||||
// |_._|
|
||||
// |
|
||||
// glyph_size.x
|
||||
//
|
||||
// In order to get the top left of the glyph, we compute an offset based on
|
||||
// the bearings. The Y bearing is the distance from the bottom of the cell
|
||||
// to the top of the glyph, so we subtract it from the cell height to get
|
||||
// the y offset. The X bearing is the distance from the left of the cell
|
||||
// to the left of the glyph, so it works as the x offset directly.
|
||||
|
||||
float2 size = float2(in.glyph_size);
|
||||
float2 offset = float2(in.bearings);
|
||||
|
||||
offset.y = uniforms.cell_size.y - offset.y;
|
||||
|
||||
// Calculate the final position of the cell which uses our glyph size
|
||||
// and glyph offset to create the correct bounding box for the glyph.
|
||||
cell_pos = cell_pos + size * corner + offset;
|
||||
out.position =
|
||||
uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
|
||||
|
||||
// Calculate the texture coordinate in pixels. This is NOT normalized
|
||||
// (between 0.0 and 1.0), and does not need to be, since the texture will
|
||||
// be sampled with pixel coordinate mode.
|
||||
out.tex_coord = float2(in.glyph_pos) + float2(in.glyph_size) * corner;
|
||||
|
||||
// Get our color. We always fetch a linearized version to
|
||||
// make it easier to handle minimum contrast calculations.
|
||||
out.color = load_color(
|
||||
in.color,
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
|
||||
// Get the BG color
|
||||
out.bg_color = load_color(
|
||||
bg_colors[in.grid_pos.y * uniforms.grid_size.x + in.grid_pos.x],
|
||||
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
|
||||
// with the background.
|
||||
if (uniforms.min_contrast > 1.0f && (in.bools & NO_MIN_CONTRAST) == 0) {
|
||||
// Ensure our minimum contrast
|
||||
out.color = contrasted_color(uniforms.min_contrast, out.color, out.bg_color);
|
||||
}
|
||||
|
||||
// Check if current position is under cursor (including wide cursor)
|
||||
bool is_cursor_pos = (
|
||||
in.grid_pos.x == uniforms.cursor_pos.x ||
|
||||
uniforms.cursor_wide &&
|
||||
in.grid_pos.x == uniforms.cursor_pos.x + 1
|
||||
) && in.grid_pos.y == uniforms.cursor_pos.y;
|
||||
|
||||
// If this cell is the cursor cell, but we're not processing
|
||||
// the cursor glyph itself, then we need to change the color.
|
||||
if ((in.bools & IS_CURSOR_GLYPH) == 0 && is_cursor_pos) {
|
||||
out.color = load_color(
|
||||
uniforms.cursor_color,
|
||||
uniforms.use_display_p3,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 cell_text_fragment(
|
||||
CellTextVertexOut in [[stage_in]],
|
||||
texture2d<float> textureGrayscale [[texture(0)]],
|
||||
texture2d<float> textureColor [[texture(1)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(
|
||||
coord::pixel,
|
||||
address::clamp_to_edge,
|
||||
filter::nearest
|
||||
);
|
||||
|
||||
switch (in.atlas) {
|
||||
default:
|
||||
case ATLAS_GRAYSCALE: {
|
||||
// Our input color is always linear.
|
||||
float4 color = in.color;
|
||||
|
||||
// If we're not doing linear blending, then we need to
|
||||
// re-apply the gamma encoding to our color manually.
|
||||
//
|
||||
// Since the alpha is premultiplied, we need to divide
|
||||
// it out before unlinearizing and re-multiply it after.
|
||||
if (!uniforms.use_linear_blending) {
|
||||
color.rgb /= color.a;
|
||||
color = unlinearize(color);
|
||||
color.rgb *= color.a;
|
||||
}
|
||||
|
||||
// Fetch our alpha mask for this pixel.
|
||||
float a = textureGrayscale.sample(textureSampler, in.tex_coord).r;
|
||||
|
||||
// Linear blending weight correction corrects the alpha value to
|
||||
// produce blending results which match gamma-incorrect blending.
|
||||
if (uniforms.use_linear_correction) {
|
||||
// Short explanation of how this works:
|
||||
//
|
||||
// We get the luminances of the foreground and background colors,
|
||||
// and then unlinearize them and perform blending on them. This
|
||||
// gives us our desired luminance, which we derive our new alpha
|
||||
// value from by mapping the range [bg_l, fg_l] to [0, 1], since
|
||||
// our final blend will be a linear interpolation from bg to fg.
|
||||
//
|
||||
// This yields virtually identical results for grayscale blending,
|
||||
// and very similar but non-identical results for color blending.
|
||||
float4 bg = in.bg_color;
|
||||
float fg_l = luminance(color.rgb);
|
||||
float bg_l = luminance(bg.rgb);
|
||||
// To avoid numbers going haywire, we don't apply correction
|
||||
// when the bg and fg luminances are within 0.001 of each other.
|
||||
if (abs(fg_l - bg_l) > 0.001) {
|
||||
float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
|
||||
a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply our whole color by the alpha mask.
|
||||
// Since we use premultiplied alpha, this is
|
||||
// the correct way to apply the mask.
|
||||
color *= a;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
case ATLAS_COLOR: {
|
||||
// For now, we assume that color glyphs
|
||||
// are already premultiplied linear colors.
|
||||
float4 color = textureColor.sample(textureSampler, in.tex_coord);
|
||||
|
||||
// If we're doing linear blending, we can return this right away.
|
||||
if (uniforms.use_linear_blending) {
|
||||
return color;
|
||||
}
|
||||
|
||||
// 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 = unlinearize(color);
|
||||
color.rgb *= color.a;
|
||||
|
||||
return color;
|
||||
}
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------------
|
||||
// Image Shader
|
||||
//-------------------------------------------------------------------
|
||||
#pragma mark - Image Shader
|
||||
|
||||
struct ImageVertexIn {
|
||||
// The grid coordinates (x, y) where x < columns and y < rows where
|
||||
// the image will be rendered. It will be rendered from the top left.
|
||||
float2 grid_pos [[attribute(0)]];
|
||||
|
||||
// Offset in pixels from the top-left of the cell to make the top-left
|
||||
// corner of the image.
|
||||
float2 cell_offset [[attribute(1)]];
|
||||
|
||||
// The source rectangle of the texture to sample from.
|
||||
float4 source_rect [[attribute(2)]];
|
||||
|
||||
// The final width/height of the image in pixels.
|
||||
float2 dest_size [[attribute(3)]];
|
||||
};
|
||||
|
||||
struct ImageVertexOut {
|
||||
float4 position [[position]];
|
||||
float2 tex_coord;
|
||||
};
|
||||
|
||||
vertex ImageVertexOut image_vertex(
|
||||
uint vid [[vertex_id]],
|
||||
ImageVertexIn in [[stage_in]],
|
||||
texture2d<uint> image [[texture(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
// We use a triangle strip with 4 vertices to render quads,
|
||||
// so we determine which corner of the cell this vertex is in
|
||||
// based on the vertex ID.
|
||||
//
|
||||
// 0 --> 1
|
||||
// | .'|
|
||||
// | / |
|
||||
// | L |
|
||||
// 2 --> 3
|
||||
//
|
||||
// 0 = top-left (0, 0)
|
||||
// 1 = top-right (1, 0)
|
||||
// 2 = bot-left (0, 1)
|
||||
// 3 = bot-right (1, 1)
|
||||
float2 corner;
|
||||
corner.x = float(vid == 1 || vid == 3);
|
||||
corner.y = float(vid == 2 || vid == 3);
|
||||
|
||||
// The texture coordinates start at our source x/y
|
||||
// and add the width/height depending on the corner.
|
||||
//
|
||||
// We don't need to normalize because we use pixel addressing for our sampler.
|
||||
float2 tex_coord = in.source_rect.xy;
|
||||
tex_coord += in.source_rect.zw * corner;
|
||||
|
||||
ImageVertexOut out;
|
||||
|
||||
// The position of our image starts at the top-left of the grid cell and
|
||||
// adds the source rect width/height components.
|
||||
float2 image_pos = (uniforms.cell_size * in.grid_pos) + in.cell_offset;
|
||||
image_pos += in.dest_size * corner;
|
||||
|
||||
out.position =
|
||||
uniforms.projection_matrix * float4(image_pos.x, image_pos.y, 0.0f, 1.0f);
|
||||
out.tex_coord = tex_coord;
|
||||
return out;
|
||||
}
|
||||
|
||||
fragment float4 image_fragment(
|
||||
ImageVertexOut in [[stage_in]],
|
||||
texture2d<float> image [[texture(0)]],
|
||||
constant Uniforms& uniforms [[buffer(1)]]
|
||||
) {
|
||||
constexpr sampler textureSampler(
|
||||
coord::pixel,
|
||||
address::clamp_to_edge,
|
||||
filter::linear
|
||||
);
|
||||
|
||||
float4 rgba = image.sample(textureSampler, in.tex_coord);
|
||||
|
||||
if (!uniforms.use_linear_blending) {
|
||||
rgba = unlinearize(rgba);
|
||||
}
|
||||
|
||||
rgba.rgb *= rgba.a;
|
||||
|
||||
return rgba;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ pub enum Backend {
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// The default backend for the target. Metal/WebGL are out of scope for
|
||||
/// the Rust port (Linux only), so this is always OpenGL on non-Darwin.
|
||||
/// The default backend for the target (Metal on Apple platforms, OpenGL
|
||||
/// otherwise). WebGL is only a name here: the wasm build has no GL renderer
|
||||
/// in this crate. This is what [`crate::Api`] / [`crate::Renderer`] are
|
||||
/// instantiated with.
|
||||
pub fn default_for_target() -> Backend {
|
||||
if cfg!(target_vendor = "apple") {
|
||||
Backend::Metal
|
||||
@@ -27,3 +29,25 @@ pub enum Health {
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_backend_matches_selected_api() {
|
||||
// `renderer.zig` picks the graphics API from the backend.
|
||||
if cfg!(target_vendor = "apple") {
|
||||
assert_eq!(Backend::default_for_target(), Backend::Metal);
|
||||
} else {
|
||||
assert_eq!(Backend::default_for_target(), Backend::Opengl);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_values_match_ghostty_h() {
|
||||
// `GHOSTTY_RENDERER_HEALTH_{OK,ERROR}`.
|
||||
assert_eq!(Health::Healthy as i32, 0);
|
||||
assert_eq!(Health::Unhealthy as i32, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
//! The CVDisplayLink used to drive the rendering loop in sync with the
|
||||
//! display (`pkg/macos/video/display_link.zig`, used by `generic.zig`).
|
||||
//!
|
||||
//! This is only functional on macOS. On every other platform
|
||||
//! [`DisplayLink`] is an uninhabited type, so a `None` is all the generic
|
||||
//! renderer ever has (Zig: `DisplayLink = void`).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// The error of display link operations (`DisplayLink.Error`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("invalid operation")]
|
||||
InvalidOperation,
|
||||
}
|
||||
|
||||
/// Called on every vsync from the display link's thread.
|
||||
pub type Callback = Arc<dyn Fn() + Send + Sync>;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use self::macos::DisplayLink;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub use self::unsupported::DisplayLink;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod unsupported {
|
||||
use std::convert::Infallible;
|
||||
|
||||
use super::{Callback, Error};
|
||||
|
||||
/// There is no display link on this platform.
|
||||
pub struct DisplayLink(Infallible);
|
||||
|
||||
impl DisplayLink {
|
||||
pub fn create_with_active_cg_displays() -> Option<DisplayLink> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), Error> {
|
||||
match self.0 {}
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<(), Error> {
|
||||
match self.0 {}
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
match self.0 {}
|
||||
}
|
||||
|
||||
pub fn set_current_cg_display(&self, _display_id: u32) -> Result<(), Error> {
|
||||
match self.0 {}
|
||||
}
|
||||
|
||||
pub fn set_output_callback(&self, _callback: Callback) -> Result<(), Error> {
|
||||
match self.0 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(unsafe_code, deprecated)]
|
||||
mod macos {
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
ptr::NonNull,
|
||||
sync::{Mutex, PoisonError},
|
||||
};
|
||||
|
||||
use objc2_core_foundation::CFRetained;
|
||||
use objc2_core_video::{CVDisplayLink, CVOptionFlags, CVReturn, CVTimeStamp, kCVReturnSuccess};
|
||||
|
||||
use super::{Callback, Error};
|
||||
|
||||
/// A CVDisplayLink.
|
||||
pub struct DisplayLink {
|
||||
link: CFRetained<CVDisplayLink>,
|
||||
|
||||
/// The callback handed to CoreVideo as `user_info` (a leaked
|
||||
/// `Box<Callback>`), freed when the link is dropped or the callback is
|
||||
/// replaced.
|
||||
callback: Mutex<Option<NonNull<Callback>>>,
|
||||
}
|
||||
|
||||
// SAFETY: CVDisplayLink is a thread-safe CoreVideo object; the callback
|
||||
// pointer is only freed with the lock held, after the link is stopped.
|
||||
unsafe impl Send for DisplayLink {}
|
||||
// SAFETY: see above.
|
||||
unsafe impl Sync for DisplayLink {}
|
||||
|
||||
fn check(r: CVReturn) -> Result<(), Error> {
|
||||
if r == kCVReturnSuccess {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
|
||||
impl DisplayLink {
|
||||
/// `createWithActiveCGDisplays`.
|
||||
pub fn create_with_active_cg_displays() -> Option<DisplayLink> {
|
||||
let mut out: *mut CVDisplayLink = std::ptr::null_mut();
|
||||
// SAFETY: `out` is a valid out pointer.
|
||||
let r =
|
||||
unsafe { CVDisplayLink::create_with_active_cg_displays(NonNull::from(&mut out)) };
|
||||
if r != kCVReturnSuccess {
|
||||
return None;
|
||||
}
|
||||
let ptr = NonNull::new(out)?;
|
||||
// SAFETY: the function returns a +1 retained link.
|
||||
let link = unsafe { CFRetained::from_raw(ptr) };
|
||||
Some(DisplayLink {
|
||||
link,
|
||||
callback: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), Error> {
|
||||
check(self.link.start())
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<(), Error> {
|
||||
check(self.link.stop())
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.link.is_running()
|
||||
}
|
||||
|
||||
pub fn set_current_cg_display(&self, display_id: u32) -> Result<(), Error> {
|
||||
check(self.link.set_current_cg_display(display_id))
|
||||
}
|
||||
|
||||
/// Set the callback called on every vsync.
|
||||
///
|
||||
/// Note: this purposely throws away a ton of arguments we don't need.
|
||||
pub fn set_output_callback(&self, callback: Callback) -> Result<(), Error> {
|
||||
let boxed = NonNull::from(Box::leak(Box::new(callback)));
|
||||
// SAFETY: `trampoline` matches the callback signature and
|
||||
// `user_info` stays valid until the link is dropped.
|
||||
let r = unsafe {
|
||||
self.link
|
||||
.set_output_callback(Some(trampoline), boxed.as_ptr().cast::<c_void>())
|
||||
};
|
||||
if let Err(e) = check(r) {
|
||||
// SAFETY: we just leaked this box and CoreVideo rejected it.
|
||||
drop(unsafe { Box::from_raw(boxed.as_ptr()) });
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
let old = self
|
||||
.callback
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.replace(boxed);
|
||||
if let Some(old) = old {
|
||||
// Make sure the old callback can't be running anymore.
|
||||
let _ = self.link.stop();
|
||||
// SAFETY: the pointer came from `Box::leak` above and the
|
||||
// link no longer references it (replaced + stopped).
|
||||
drop(unsafe { Box::from_raw(old.as_ptr()) });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DisplayLink {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.link.stop();
|
||||
if let Some(cb) = self
|
||||
.callback
|
||||
.get_mut()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.take()
|
||||
{
|
||||
// SAFETY: the link is stopped and released right after.
|
||||
drop(unsafe { Box::from_raw(cb.as_ptr()) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C-unwind" fn trampoline(
|
||||
_display_link: NonNull<CVDisplayLink>,
|
||||
_in_now: NonNull<CVTimeStamp>,
|
||||
_in_output_time: NonNull<CVTimeStamp>,
|
||||
_flags_in: CVOptionFlags,
|
||||
_flags_out: NonNull<CVOptionFlags>,
|
||||
user_info: *mut c_void,
|
||||
) -> CVReturn {
|
||||
// SAFETY: `user_info` is the leaked `Box<Callback>` set in
|
||||
// `set_output_callback`, alive until the link is stopped.
|
||||
if let Some(cb) = unsafe { user_info.cast::<Callback>().as_ref() } {
|
||||
cb();
|
||||
}
|
||||
kCVReturnSuccess
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,11 @@ use std::{
|
||||
collections::HashSet,
|
||||
sync::{
|
||||
Arc,
|
||||
Condvar,
|
||||
Mutex,
|
||||
MutexGuard,
|
||||
PoisonError,
|
||||
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
@@ -92,56 +93,58 @@ use crate::{
|
||||
backend::Health,
|
||||
cell::{self, Contents, Key as CellKey},
|
||||
cursor::{self, Style as CursorStyle},
|
||||
display_link::DisplayLink,
|
||||
graphics_api::{
|
||||
ApiError,
|
||||
Attachment,
|
||||
AttachmentTarget,
|
||||
Draw,
|
||||
FrameCompletion,
|
||||
GpuBuffer,
|
||||
GpuFrame,
|
||||
GpuRenderPass,
|
||||
GpuShaders,
|
||||
GpuTarget,
|
||||
GpuTexture,
|
||||
GraphicsApi,
|
||||
Primitive,
|
||||
Step,
|
||||
TextureKind,
|
||||
},
|
||||
image::{self, Gpu},
|
||||
link,
|
||||
math,
|
||||
opengl::{
|
||||
self,
|
||||
OpenGL,
|
||||
Sampler,
|
||||
Target,
|
||||
Texture,
|
||||
buffer::Buffer,
|
||||
gl::{self, Gl, Primitive},
|
||||
render_pass::{Attachment, AttachmentTarget, Draw, Step},
|
||||
shaders::{
|
||||
BgImage,
|
||||
BgImageFit,
|
||||
BgImageInfo,
|
||||
BgImagePosition,
|
||||
Bools,
|
||||
CellBg,
|
||||
CellText,
|
||||
CellTextAtlas,
|
||||
PaddingExtend,
|
||||
Shaders,
|
||||
Uniforms,
|
||||
},
|
||||
},
|
||||
opengl::OpenGL,
|
||||
options::Options,
|
||||
overlay::{self, Overlay},
|
||||
row::never_extend_bg,
|
||||
shader_types::{
|
||||
BgImage,
|
||||
BgImageFit,
|
||||
BgImageInfo,
|
||||
BgImagePosition,
|
||||
Bools,
|
||||
CellBg,
|
||||
CellText,
|
||||
CellTextAtlas,
|
||||
PaddingExtend,
|
||||
Uniforms,
|
||||
},
|
||||
shadertoy::{self, PathItem},
|
||||
size::{CellSize, GridSize, Padding, Size},
|
||||
state::{Preedit, SharedState},
|
||||
};
|
||||
|
||||
/// The graphics API used by the renderer on Linux.
|
||||
pub type GraphicsApi = OpenGL;
|
||||
|
||||
/// Errors surfaced by the generic renderer.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Api(#[from] opengl::Error),
|
||||
Api(#[from] ApiError),
|
||||
#[error(transparent)]
|
||||
Gl(#[from] gl::Error),
|
||||
Shaper(#[from] ghostty_font::shape::ShapeError),
|
||||
#[error(transparent)]
|
||||
Texture(#[from] opengl::texture::Error),
|
||||
#[error(transparent)]
|
||||
Shaper(#[from] ghostty_font::shaper::harfbuzz::ShapeError),
|
||||
#[error(transparent)]
|
||||
ShapeRun(#[from] ghostty_font::shaper::harfbuzz::ShapeRunError),
|
||||
ShapeRun(#[from] ghostty_font::shape::ShapeRunError),
|
||||
#[error(transparent)]
|
||||
Grid(#[from] ghostty_font::shared_grid::Error),
|
||||
#[error(transparent)]
|
||||
@@ -317,47 +320,48 @@ impl std::fmt::Debug for DerivedConfig {
|
||||
/// With OpenGL `swap_chain_count == 1` and frames complete synchronously
|
||||
/// (`glFinish`), so the semaphore of the Zig swap chain is not needed: the
|
||||
/// draw mutex already serializes `draw_frame`.
|
||||
struct FrameState {
|
||||
uniforms: Buffer<Uniforms>,
|
||||
cells: Buffer<CellText>,
|
||||
cells_bg: Buffer<CellBg>,
|
||||
struct FrameState<G: GraphicsApi> {
|
||||
uniforms: G::Buffer<G::GpuUniforms>,
|
||||
cells: G::Buffer<CellText>,
|
||||
cells_bg: G::Buffer<CellBg>,
|
||||
|
||||
grayscale: Texture,
|
||||
grayscale: G::Texture,
|
||||
grayscale_modified: usize,
|
||||
color: Texture,
|
||||
color: G::Texture,
|
||||
color_modified: usize,
|
||||
|
||||
target: Arc<Target>,
|
||||
target: Arc<G::Target>,
|
||||
/// See property of same name on Renderer for explanation.
|
||||
target_config_modified: usize,
|
||||
|
||||
/// Buffer with the vertex data for our background image.
|
||||
bg_image_buffer: Buffer<BgImage>,
|
||||
bg_image_buffer: G::Buffer<BgImage>,
|
||||
/// See property of same name on Renderer for explanation.
|
||||
bg_image_buffer_modified: usize,
|
||||
|
||||
/// Custom shader state, this is None if we have no custom shaders.
|
||||
custom_shader_state: Option<CustomShaderState>,
|
||||
custom_shader_state: Option<CustomShaderState<G>>,
|
||||
|
||||
/// The swap chain's atlas epoch this frame state last synced with.
|
||||
atlas_epoch: usize,
|
||||
}
|
||||
|
||||
impl FrameState {
|
||||
fn new(api: &OpenGL, custom_shaders: bool) -> Result<FrameState, Error> {
|
||||
let gl = api.gl().ok_or(opengl::Error::NoContext)?;
|
||||
|
||||
impl<G: GraphicsApi> FrameState<G> {
|
||||
fn new(api: &G, custom_shaders: bool) -> Result<FrameState<G>, Error> {
|
||||
// Uniform buffer contains exactly 1 uniform struct. The
|
||||
// uniform data will be zeroed so this must be set before
|
||||
// a frame is drawn.
|
||||
let uniforms = Buffer::<Uniforms>::init(gl, api.uniform_buffer_options(), 1)?;
|
||||
let uniforms = api.init_buffer::<G::GpuUniforms>(1)?;
|
||||
|
||||
// Create GPU buffers for our cells.
|
||||
//
|
||||
// We start them off with a size of 1, which will of course be
|
||||
// too small, but they will be resized as needed.
|
||||
let cells = Buffer::<CellText>::init(gl, api.fg_buffer_options(), 1)?;
|
||||
let cells_bg = Buffer::<CellBg>::init(gl, api.bg_buffer_options(), 1)?;
|
||||
let cells = api.init_buffer::<CellText>(1)?;
|
||||
let cells_bg = api.init_buffer::<CellBg>(1)?;
|
||||
|
||||
// Create a GPU buffer for our background image info.
|
||||
let bg_image_buffer = Buffer::<BgImage>::init(gl, api.bg_image_buffer_options(), 1)?;
|
||||
let bg_image_buffer = api.init_buffer::<BgImage>(1)?;
|
||||
|
||||
// Initialize our textures for our font atlas.
|
||||
//
|
||||
@@ -393,10 +397,11 @@ impl FrameState {
|
||||
bg_image_buffer,
|
||||
bg_image_buffer_modified: 0,
|
||||
custom_shader_state,
|
||||
atlas_epoch: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn resize(&mut self, api: &OpenGL, width: usize, height: usize) -> Result<(), Error> {
|
||||
fn resize(&mut self, api: &G, width: usize, height: usize) -> Result<(), Error> {
|
||||
if let Some(state) = &mut self.custom_shader_state {
|
||||
state.resize(api, width, height)?;
|
||||
}
|
||||
@@ -406,35 +411,32 @@ impl FrameState {
|
||||
}
|
||||
|
||||
/// State relevant to our custom shaders if we have any.
|
||||
struct CustomShaderState {
|
||||
struct CustomShaderState<G: GraphicsApi> {
|
||||
/// When we have a custom shader state, we maintain a front
|
||||
/// and back texture which we use as a swap chain to render
|
||||
/// between when multiple custom shaders are defined.
|
||||
front_texture: Texture,
|
||||
back_texture: Texture,
|
||||
front_texture: G::Texture,
|
||||
back_texture: G::Texture,
|
||||
|
||||
/// Shadertoy uses a sampler for accessing the various channel textures.
|
||||
/// Samplers are immutable and describe sampling properties so we can
|
||||
/// share the sampler across front/back textures.
|
||||
sampler: Sampler,
|
||||
sampler: G::Sampler,
|
||||
|
||||
uniforms: Buffer<shadertoy::Uniforms>,
|
||||
uniforms: G::Buffer<shadertoy::Uniforms>,
|
||||
}
|
||||
|
||||
impl CustomShaderState {
|
||||
fn new(api: &OpenGL) -> Result<CustomShaderState, Error> {
|
||||
let gl = api.gl().ok_or(opengl::Error::NoContext)?;
|
||||
|
||||
impl<G: GraphicsApi> CustomShaderState<G> {
|
||||
fn new(api: &G) -> Result<CustomShaderState<G>, Error> {
|
||||
// Create a GPU buffer to hold our uniforms.
|
||||
let uniforms = Buffer::<shadertoy::Uniforms>::init(gl, api.uniform_buffer_options(), 1)?;
|
||||
let uniforms = api.init_buffer::<shadertoy::Uniforms>(1)?;
|
||||
|
||||
// Initialize the front and back textures at 1x1 px, this
|
||||
// is slightly wasteful but it's only done once so whatever.
|
||||
let front_texture = Texture::init(gl, api.texture_options(), 1, 1, None)?;
|
||||
let back_texture = Texture::init(gl, api.texture_options(), 1, 1, None)?;
|
||||
let front_texture = api.init_texture(TextureKind::CustomShader, 1, 1, None)?;
|
||||
let back_texture = api.init_texture(TextureKind::CustomShader, 1, 1, None)?;
|
||||
|
||||
let sampler = Sampler::init(gl, api.sampler_options())
|
||||
.map_err(|_| gl::Error::CreateFailed("sampler".into()))?;
|
||||
let sampler = api.init_sampler()?;
|
||||
|
||||
Ok(CustomShaderState {
|
||||
front_texture,
|
||||
@@ -449,18 +451,35 @@ impl CustomShaderState {
|
||||
std::mem::swap(&mut self.front_texture, &mut self.back_texture);
|
||||
}
|
||||
|
||||
fn resize(&mut self, api: &OpenGL, width: usize, height: usize) -> Result<(), Error> {
|
||||
let gl = api.gl().ok_or(opengl::Error::NoContext)?;
|
||||
self.front_texture = Texture::init(gl, api.texture_options(), width, height, None)?;
|
||||
self.back_texture = Texture::init(gl, api.texture_options(), width, height, None)?;
|
||||
fn resize(&mut self, api: &G, width: usize, height: usize) -> Result<(), Error> {
|
||||
self.front_texture = api.init_texture(TextureKind::CustomShader, width, height, None)?;
|
||||
self.back_texture = api.init_texture(TextureKind::CustomShader, width, height, None)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The frame slots of a [`SwapChain`], shared with the frame completion
|
||||
/// callbacks (which may run on another thread and hand the frame state back).
|
||||
struct SwapChainInner<G: GraphicsApi> {
|
||||
/// `None` while the slot's frame is in flight (or being drawn).
|
||||
slots: Mutex<Vec<Option<FrameState<G>>>>,
|
||||
/// Signalled when a frame is returned (Zig: `frame_sema.post`).
|
||||
returned: Condvar,
|
||||
/// Bumped to force every frame's atlas textures to re-sync, including
|
||||
/// the ones that are in flight right now (see `FrameState::atlas_epoch`).
|
||||
atlas_epoch: AtomicUsize,
|
||||
}
|
||||
|
||||
/// Swap chain which maintains multiple copies of the state needed to render
|
||||
/// a frame (always one for OpenGL, see [`FrameState`]).
|
||||
struct SwapChain {
|
||||
frames: Vec<Option<FrameState>>,
|
||||
/// a frame, so that we can start building the next frame while the previous
|
||||
/// frame is still being processed on the GPU (always one for OpenGL, see
|
||||
/// [`FrameState`]; three for Metal).
|
||||
///
|
||||
/// Zig guards the frames with a semaphore; here taking a frame waits (on a
|
||||
/// condition variable) until its slot has been handed back by the frame's
|
||||
/// completion callback.
|
||||
struct SwapChain<G: GraphicsApi> {
|
||||
inner: Arc<SwapChainInner<G>>,
|
||||
/// Index of the most recently used frame state struct.
|
||||
frame_index: usize,
|
||||
/// Set to true when deinited, so that a defunct swap chain is ignored
|
||||
@@ -468,14 +487,18 @@ struct SwapChain {
|
||||
defunct: bool,
|
||||
}
|
||||
|
||||
impl SwapChain {
|
||||
fn new(api: &OpenGL, custom_shaders: bool) -> Result<SwapChain, Error> {
|
||||
let mut frames = Vec::with_capacity(opengl::SWAP_CHAIN_COUNT);
|
||||
for _ in 0..opengl::SWAP_CHAIN_COUNT {
|
||||
impl<G: GraphicsApi> SwapChain<G> {
|
||||
fn new(api: &G, custom_shaders: bool) -> Result<SwapChain<G>, Error> {
|
||||
let mut frames = Vec::with_capacity(G::SWAP_CHAIN_COUNT);
|
||||
for _ in 0..G::SWAP_CHAIN_COUNT {
|
||||
frames.push(Some(FrameState::new(api, custom_shaders)?));
|
||||
}
|
||||
Ok(SwapChain {
|
||||
frames,
|
||||
inner: Arc::new(SwapChainInner {
|
||||
slots: Mutex::new(frames),
|
||||
returned: Condvar::new(),
|
||||
atlas_epoch: AtomicUsize::new(0),
|
||||
}),
|
||||
frame_index: 0,
|
||||
defunct: false,
|
||||
})
|
||||
@@ -486,39 +509,91 @@ impl SwapChain {
|
||||
return;
|
||||
}
|
||||
self.defunct = true;
|
||||
self.frames.clear();
|
||||
|
||||
// Wait for all of our inflight draws to complete
|
||||
// so that we can cleanly deinit our GPU state.
|
||||
let mut slots = lock(&self.inner.slots);
|
||||
while slots.iter().any(Option::is_none) {
|
||||
slots = self
|
||||
.inner
|
||||
.returned
|
||||
.wait(slots)
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
}
|
||||
slots.clear();
|
||||
}
|
||||
|
||||
/// Get the next frame state to draw to.
|
||||
fn next_frame(&mut self) -> Result<(usize, FrameState), Error> {
|
||||
if self.defunct || self.frames.is_empty() {
|
||||
/// Get the next frame state to draw to. This waits until the frame is
|
||||
/// available. It must always be paired with a [`SwapChain::release_frame`]
|
||||
/// (directly, or through the frame completion callback).
|
||||
fn next_frame(&mut self) -> Result<(usize, FrameState<G>), Error> {
|
||||
if self.defunct {
|
||||
return Err(Error::Defunct);
|
||||
}
|
||||
self.frame_index = (self.frame_index + 1) % self.frames.len();
|
||||
let frame = self.frames[self.frame_index].take().ok_or(Error::Defunct)?;
|
||||
Ok((self.frame_index, frame))
|
||||
let mut slots = lock(&self.inner.slots);
|
||||
if slots.is_empty() {
|
||||
return Err(Error::Defunct);
|
||||
}
|
||||
self.frame_index = (self.frame_index + 1) % slots.len();
|
||||
let idx = self.frame_index;
|
||||
let mut frame = loop {
|
||||
if let Some(frame) = slots[idx].take() {
|
||||
break frame;
|
||||
}
|
||||
slots = self
|
||||
.inner
|
||||
.returned
|
||||
.wait(slots)
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
};
|
||||
drop(slots);
|
||||
|
||||
// Update all our textures so that they sync on the next frame, if
|
||||
// the font grid changed since this frame state was last used.
|
||||
let epoch = self.inner.atlas_epoch.load(Ordering::SeqCst);
|
||||
if frame.atlas_epoch != epoch {
|
||||
frame.atlas_epoch = epoch;
|
||||
frame.grayscale_modified = 0;
|
||||
frame.color_modified = 0;
|
||||
}
|
||||
Ok((idx, frame))
|
||||
}
|
||||
|
||||
fn release_frame(&mut self, idx: usize, frame: FrameState) {
|
||||
if let Some(slot) = self.frames.get_mut(idx) {
|
||||
/// A handle that hands a frame state back to its slot.
|
||||
fn releaser(&self) -> Arc<SwapChainInner<G>> {
|
||||
Arc::clone(&self.inner)
|
||||
}
|
||||
|
||||
fn release_frame(&self, idx: usize, frame: FrameState<G>) {
|
||||
SwapChainInner::release(&self.inner, idx, frame);
|
||||
}
|
||||
|
||||
/// Make every frame re-upload its atlas textures on its next use.
|
||||
fn invalidate_atlases(&self) {
|
||||
self.inner.atlas_epoch.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl<G: GraphicsApi> SwapChainInner<G> {
|
||||
/// Hand a frame state back to its slot (Zig: `releaseFrame`).
|
||||
fn release(this: &Arc<Self>, idx: usize, frame: FrameState<G>) {
|
||||
let mut slots = lock(&this.slots);
|
||||
if let Some(slot) = slots.get_mut(idx) {
|
||||
*slot = Some(frame);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable access to all frames (for invalidating atlas textures).
|
||||
fn frames_mut(&mut self) -> impl Iterator<Item = &mut FrameState> {
|
||||
self.frames.iter_mut().flatten()
|
||||
drop(slots);
|
||||
this.returned.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// The background image state (`bg_image: ?imagepkg.Image`).
|
||||
type BgImageState = Option<image::Image>;
|
||||
type BgImageState<G> = Option<image::Image<G>>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Renderer state
|
||||
|
||||
/// State protected by the draw mutex: everything `draw_frame` reads.
|
||||
struct DrawState {
|
||||
struct DrawState<G: GraphicsApi> {
|
||||
/// The configuration we need derived from the main config.
|
||||
config: DerivedConfig,
|
||||
|
||||
@@ -566,13 +641,13 @@ struct DrawState {
|
||||
font_grid: Arc<SharedGrid>,
|
||||
|
||||
/// The images that we may render.
|
||||
images: image::State,
|
||||
images: image::State<G>,
|
||||
|
||||
/// True when a debug overlay image is currently present (Zig: `overlay != null`).
|
||||
has_overlay: bool,
|
||||
|
||||
/// Background image, if we have one.
|
||||
bg_image: BgImageState,
|
||||
bg_image: BgImageState<G>,
|
||||
/// Background image vertex buffer.
|
||||
bg_image_buffer: BgImage,
|
||||
/// This value is used to force-update the swap chain copy
|
||||
@@ -580,10 +655,10 @@ struct DrawState {
|
||||
bg_image_buffer_modified: usize,
|
||||
|
||||
/// Graphics API state.
|
||||
api: OpenGL,
|
||||
api: G,
|
||||
|
||||
/// Our swap chain (multiple buffering)
|
||||
swap_chain: SwapChain,
|
||||
swap_chain: SwapChain<G>,
|
||||
|
||||
/// This value is used to force-update swap chain targets in the
|
||||
/// event of a config change that requires it (such as blending mode).
|
||||
@@ -597,7 +672,7 @@ struct DrawState {
|
||||
has_custom_shaders: bool,
|
||||
|
||||
/// Our shader pipelines. None while defunct (display unrealized).
|
||||
shaders: Option<Shaders>,
|
||||
shaders: Option<G::Shaders>,
|
||||
}
|
||||
|
||||
/// State only touched by the renderer thread.
|
||||
@@ -642,17 +717,22 @@ struct ThreadState {
|
||||
}
|
||||
|
||||
/// The generic renderer. See the module docs for the threading model.
|
||||
pub struct Renderer {
|
||||
draw: Mutex<DrawState>,
|
||||
pub struct Renderer<G: GraphicsApi = OpenGL> {
|
||||
draw: Mutex<DrawState<G>>,
|
||||
thread: Mutex<ThreadState>,
|
||||
|
||||
/// Health of the most recently completed frame.
|
||||
health: AtomicU8,
|
||||
health: Arc<AtomicU8>,
|
||||
|
||||
/// Mirrors `DrawState::has_custom_shaders` so the renderer thread can
|
||||
/// read it without the draw lock (`hasAnimations`).
|
||||
has_animations: AtomicBool,
|
||||
|
||||
/// The CVDisplayLink used to drive the rendering loop in sync with the
|
||||
/// display. Always `None` on platforms that don't support a display link
|
||||
/// (everything but macOS).
|
||||
display_link: Mutex<Option<DisplayLink>>,
|
||||
|
||||
/// The surface/apprt callbacks.
|
||||
host: Arc<dyn Host>,
|
||||
}
|
||||
@@ -670,8 +750,8 @@ struct PreeditRange {
|
||||
|
||||
/// Load the custom shaders (`initShaders`). Errors are logged and result in
|
||||
/// no custom shaders.
|
||||
fn load_custom_shaders(config: &DerivedConfig) -> Vec<String> {
|
||||
match shadertoy::load_from_files(&config.custom_shaders, opengl::CUSTOM_SHADER_TARGET) {
|
||||
fn load_custom_shaders<G: GraphicsApi>(config: &DerivedConfig) -> Vec<String> {
|
||||
match shadertoy::load_from_files(&config.custom_shaders, G::CUSTOM_SHADER_TARGET) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
log::warn!("error loading custom shaders err={err}");
|
||||
@@ -680,11 +760,11 @@ fn load_custom_shaders(config: &DerivedConfig) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
impl<G: GraphicsApi> DrawState<G> {
|
||||
/// Rebuild the shader pipelines (`initShaders`).
|
||||
fn init_shaders(&mut self) -> Result<(), Error> {
|
||||
// Load our custom shaders
|
||||
let custom_shaders = load_custom_shaders(&self.config);
|
||||
let custom_shaders = load_custom_shaders::<G>(&self.config);
|
||||
let has_custom_shaders = !custom_shaders.is_empty();
|
||||
|
||||
let shaders = self.api.init_shaders(&custom_shaders)?;
|
||||
@@ -778,7 +858,7 @@ impl DrawState {
|
||||
fn prep_background_image(&mut self) {
|
||||
// Then we try to load the background image if we have a path.
|
||||
if let Some(p) = &self.config.bg_image
|
||||
&& let Some(image) = load_background_image(p.path())
|
||||
&& let Some(image) = load_background_image(p.path()).map(image::Image::Pending)
|
||||
{
|
||||
// If we have an existing background image, replace it.
|
||||
// Otherwise, set this as our background image directly.
|
||||
@@ -795,21 +875,6 @@ impl DrawState {
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_background_image(&mut self, gpu: &Gpu<'_>) {
|
||||
// Make sure our bg image is uploaded if it needs to be.
|
||||
if let Some(bg) = &mut self.bg_image {
|
||||
if bg.is_unloading() {
|
||||
self.bg_image = None;
|
||||
return;
|
||||
}
|
||||
if bg.is_pending()
|
||||
&& let Err(err) = bg.upload(gpu)
|
||||
{
|
||||
log::warn!("error uploading background image err={err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update custom shader uniforms that depend on terminal state.
|
||||
///
|
||||
/// This should be called in `update_frame` when terminal state changes.
|
||||
@@ -909,7 +974,7 @@ impl DrawState {
|
||||
let cell = self.size.cell;
|
||||
let cursor_glyph = self.cells.get_cursor_glyph();
|
||||
let focused = self.focused;
|
||||
let y_is_down = opengl::CUSTOM_SHADER_Y_IS_DOWN;
|
||||
let y_is_down = G::CUSTOM_SHADER_Y_IS_DOWN;
|
||||
|
||||
let uniforms = &mut self.custom_shader_uniforms;
|
||||
|
||||
@@ -1001,8 +1066,8 @@ impl DrawState {
|
||||
/// Sync the atlas data to the given texture. This copies the bytes
|
||||
/// associated with the atlas to the given texture. If the atlas no
|
||||
/// longer fits into the texture, the texture will be resized.
|
||||
fn sync_atlas_texture(api: &OpenGL, atlas: &Atlas, texture: &mut Texture) -> Result<(), Error> {
|
||||
if atlas.size as usize > texture.width {
|
||||
fn sync_atlas_texture(api: &G, atlas: &Atlas, texture: &mut G::Texture) -> Result<(), Error> {
|
||||
if atlas.size as usize > texture.width() {
|
||||
// Reallocate (the old texture is dropped/deleted)
|
||||
*texture = api.init_atlas_texture(atlas)?;
|
||||
}
|
||||
@@ -1012,9 +1077,24 @@ impl DrawState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure our bg image is uploaded if it needs to be.
|
||||
fn upload_background_image<G: GraphicsApi>(bg_image: &mut BgImageState<G>, gpu: &Gpu<'_, G>) {
|
||||
if let Some(bg) = bg_image {
|
||||
if bg.is_unloading() {
|
||||
*bg_image = None;
|
||||
return;
|
||||
}
|
||||
if bg.is_pending()
|
||||
&& let Err(err) = bg.upload(gpu)
|
||||
{
|
||||
log::warn!("error uploading background image err={err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a background image file (`prepBackgroundImage`). Failures are
|
||||
/// logged and result in no image, as in Zig.
|
||||
fn load_background_image(path: &str) -> Option<image::Image> {
|
||||
fn load_background_image(path: &str) -> Option<image::Pending> {
|
||||
// Read it (Max size of 4 GiB, for now.)
|
||||
let contents = match std::fs::read(path) {
|
||||
Ok(c) if u32::try_from(c.len()).is_ok() => c,
|
||||
@@ -1060,12 +1140,12 @@ fn load_background_image(path: &str) -> Option<image::Image> {
|
||||
}
|
||||
};
|
||||
|
||||
Some(image::Image::Pending(image::Pending {
|
||||
Some(image::Pending {
|
||||
width,
|
||||
height,
|
||||
pixel_format: image::PixelFormat::Rgba,
|
||||
data,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/// `file_type.zig` (the subset needed here).
|
||||
@@ -1239,14 +1319,14 @@ fn shaped_cells_for_run(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
impl<G: GraphicsApi> Renderer<G> {
|
||||
/// Create the renderer (`Renderer.init`).
|
||||
///
|
||||
/// `api` must already have its GL context prepared
|
||||
/// ([`OpenGL::prepare_context`], i.e. Zig `Renderer.surfaceInit`), and the
|
||||
/// context must be current on the calling thread: swap chain buffers,
|
||||
/// `api` must already be prepared (OpenGL: [`OpenGL::prepare_context`],
|
||||
/// i.e. Zig `Renderer.surfaceInit`, with the context current on the
|
||||
/// calling thread; Metal: attached to its view): swap chain buffers,
|
||||
/// textures and shaders are created here.
|
||||
pub fn new(opts: Options<DerivedConfig>, api: OpenGL) -> Result<Renderer, Error> {
|
||||
pub fn new(opts: Options<DerivedConfig>, api: G) -> Result<Renderer<G>, Error> {
|
||||
let config = opts.config;
|
||||
let has_custom_shaders = !config.custom_shaders.is_empty();
|
||||
|
||||
@@ -1342,28 +1422,36 @@ impl Renderer {
|
||||
};
|
||||
|
||||
let has_animations = AtomicBool::new(draw.has_custom_shaders);
|
||||
|
||||
// The display link is created here but only started in `loop_enter`.
|
||||
let display_link = if cfg!(target_os = "macos") && draw.config.vsync {
|
||||
DisplayLink::create_with_active_cg_displays()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Renderer {
|
||||
display_link: Mutex::new(display_link),
|
||||
draw: Mutex::new(draw),
|
||||
thread: Mutex::new(thread),
|
||||
health: AtomicU8::new(Health::Healthy as u8),
|
||||
health: Arc::new(AtomicU8::new(Health::Healthy as u8)),
|
||||
has_animations,
|
||||
host: opts.host,
|
||||
})
|
||||
}
|
||||
|
||||
/// This is called by the GTK apprt after the surface is reinitialized
|
||||
/// This is called by the apprt after the surface is reinitialized
|
||||
/// due to any of the events mentioned in the doc comment of
|
||||
/// [`Renderer::display_unrealized`]. The GL context must be current.
|
||||
pub fn display_realized<F>(&self, get_proc_address: F) -> Result<(), Error>
|
||||
where
|
||||
F: FnMut(&str) -> *const std::ffi::c_void,
|
||||
{
|
||||
/// [`Renderer::display_unrealized`] (Zig `displayRealized`). `realize` is
|
||||
/// the API's own hook ("if our API has to do things on realize, let it":
|
||||
/// OpenGL reloads its context). The GPU context must be current.
|
||||
pub fn display_realized_with(&self, realize: impl FnOnce(&mut G)) -> Result<(), Error> {
|
||||
// Lock the draw mutex so that we can
|
||||
// safely reinitialize our GPU resources.
|
||||
let mut d = lock(&self.draw);
|
||||
|
||||
// If our API has to do things on realize, let it.
|
||||
d.api.display_realized(get_proc_address);
|
||||
realize(&mut d.api);
|
||||
|
||||
// We assume that the swap chain was deinited in
|
||||
// `display_unrealized`, in which case it should be
|
||||
@@ -1398,6 +1486,7 @@ impl Renderer {
|
||||
//
|
||||
// This will mark them as defunct so that they
|
||||
// can't be double-freed or used in draw calls.
|
||||
d.api.display_unrealized();
|
||||
d.swap_chain.deinit();
|
||||
d.shaders = None;
|
||||
d.api.clear_last_target();
|
||||
@@ -1423,10 +1512,79 @@ impl Renderer {
|
||||
|
||||
/// True if our renderer is using vsync. If true, the renderer or apprt
|
||||
/// is responsible for triggering draw_now calls to the render thread.
|
||||
/// That is the only way to trigger a `draw_frame`. (Always false for
|
||||
/// OpenGL on Linux; there is no display link.)
|
||||
/// That is the only way to trigger a `draw_frame`. (Only true on macOS
|
||||
/// with a running display link.)
|
||||
pub fn has_vsync(&self) -> bool {
|
||||
false
|
||||
lock(&self.display_link)
|
||||
.as_ref()
|
||||
.is_some_and(DisplayLink::is_running)
|
||||
}
|
||||
|
||||
/// Called by the renderer thread when it begins (`threadEnter`).
|
||||
pub fn thread_enter(&self) {
|
||||
lock(&self.draw).api.thread_enter();
|
||||
}
|
||||
|
||||
/// Called by the renderer thread when it exits (`threadExit`).
|
||||
pub fn thread_exit(&self) {
|
||||
lock(&self.draw).api.thread_exit();
|
||||
}
|
||||
|
||||
/// Called by the renderer thread when it starts the main loop
|
||||
/// (`loopEnter`). `draw_now` forces an immediate draw on the renderer
|
||||
/// thread; the display link calls it on every vsync.
|
||||
pub fn loop_enter(self: &Arc<Self>, draw_now: Arc<dyn Fn() + Send + Sync>) {
|
||||
// If our API has to do things on loop enter, let it. (Metal draws
|
||||
// synchronously from the layer's display callback.)
|
||||
let weak = Arc::downgrade(self);
|
||||
lock(&self.draw).api.loop_enter(Arc::new(move || {
|
||||
if let Some(renderer) = weak.upgrade()
|
||||
&& let Err(err) = renderer.draw_frame(true)
|
||||
{
|
||||
log::warn!("Error drawing frame in display callback, err={err}");
|
||||
}
|
||||
}));
|
||||
|
||||
// If we don't support a display link we have no work to do.
|
||||
let guard = lock(&self.display_link);
|
||||
let Some(link) = guard.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// To setup the display link we set our callback and we can start it
|
||||
// immediately.
|
||||
if let Err(err) = link.set_output_callback(draw_now) {
|
||||
log::warn!("error setting display link callback err={err}");
|
||||
return;
|
||||
}
|
||||
let _ = link.start();
|
||||
}
|
||||
|
||||
/// Called by the renderer thread when it exits the main loop
|
||||
/// (`loopExit`).
|
||||
pub fn loop_exit(&self) {
|
||||
// If our API has to do things on loop exit, let it.
|
||||
lock(&self.draw).api.loop_exit();
|
||||
|
||||
// Stop our display link. If this fails its okay it just means
|
||||
// that we either never started it or the view its attached to
|
||||
// is gone which is fine.
|
||||
if let Some(link) = lock(&self.display_link).as_ref() {
|
||||
let _ = link.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when we get an updated display ID for our display link
|
||||
/// (`setMacOSDisplayID`).
|
||||
pub fn set_macos_display_id(&self, id: u32) {
|
||||
let guard = lock(&self.display_link);
|
||||
let Some(link) = guard.as_ref() else {
|
||||
return;
|
||||
};
|
||||
log::info!("updating display link display id={id}");
|
||||
if let Err(err) = link.set_current_cg_display(id) {
|
||||
log::warn!("error setting display link display id err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The health of the most recently completed frame.
|
||||
@@ -1454,13 +1612,31 @@ impl Renderer {
|
||||
|
||||
// Flag that we need to update our custom shaders
|
||||
d.custom_shader_focused_changed = true;
|
||||
|
||||
// If we're not focused, then we want to stop the display link
|
||||
// because it is a waste of resources and we can move to pure
|
||||
// change-driven updates.
|
||||
if let Some(link) = lock(&self.display_link).as_ref() {
|
||||
let _ = if focus { link.start() } else { link.stop() };
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback when the window is visible or occluded.
|
||||
///
|
||||
/// Must be called on the render thread. (There is no display link on
|
||||
/// Linux, so there is nothing to do.)
|
||||
pub fn set_visible(&self, _visible: bool) {}
|
||||
/// Must be called on the render thread.
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
// If we're not visible, then we want to stop the display link
|
||||
// because it is a waste of resources and we can move to pure
|
||||
// change-driven updates.
|
||||
let focused = lock(&self.draw).focused;
|
||||
if let Some(link) = lock(&self.display_link).as_ref() {
|
||||
let _ = if visible && focused {
|
||||
link.start()
|
||||
} else {
|
||||
link.stop()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the new font grid.
|
||||
///
|
||||
@@ -1477,10 +1653,7 @@ impl Renderer {
|
||||
d.font_grid = grid;
|
||||
|
||||
// Update all our textures so that they sync on the next frame.
|
||||
for frame in d.swap_chain.frames_mut() {
|
||||
frame.grayscale_modified = 0;
|
||||
frame.color_modified = 0;
|
||||
}
|
||||
d.swap_chain.invalidate_atlases();
|
||||
|
||||
d.grid_metrics = metrics;
|
||||
|
||||
@@ -1561,7 +1734,8 @@ impl Renderer {
|
||||
|
||||
if blending_changed {
|
||||
// We update our API's blending mode.
|
||||
d.api.blending = d.config.blending;
|
||||
let blending = d.config.blending;
|
||||
d.api.set_blending(blending);
|
||||
// And indicate that we need to reinitialize our shaders.
|
||||
d.reinitialize_shaders = true;
|
||||
// And indicate that our swap chain targets need to
|
||||
@@ -1602,31 +1776,55 @@ impl Renderer {
|
||||
t.search_matches_dirty = true;
|
||||
}
|
||||
|
||||
/// The callback the graphics API calls when a frame is completed
|
||||
/// (`renderer.frameCompleted`). Metal calls it from the command buffer's
|
||||
/// completion handler, so it owns handles instead of borrowing `self`.
|
||||
fn frame_completion(&self) -> FrameCompletion {
|
||||
let health = Arc::clone(&self.health);
|
||||
let host = Arc::clone(&self.host);
|
||||
Arc::new(move |h| frame_completed(&health, host.as_ref(), h))
|
||||
}
|
||||
}
|
||||
|
||||
/// Record the health of a completed frame and notify the surface on change.
|
||||
fn frame_completed(current: &AtomicU8, host: &dyn Host, health: Health) {
|
||||
// If our health value hasn't changed, then we do nothing. We don't
|
||||
// do a cmpxchg here because strict atomicity isn't important.
|
||||
if current.load(Ordering::SeqCst) != health as u8 {
|
||||
current.store(health as u8, Ordering::SeqCst);
|
||||
|
||||
// Our health value changed, so we notify the surface so that it
|
||||
// can do something about it.
|
||||
host.renderer_health(health);
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer<OpenGL> {
|
||||
/// This is called by the GTK apprt after the surface is reinitialized
|
||||
/// due to any of the events mentioned in the doc comment of
|
||||
/// [`Renderer::display_unrealized`]. The GL context must be current.
|
||||
pub fn display_realized<F>(&self, get_proc_address: F) -> Result<(), Error>
|
||||
where
|
||||
F: FnMut(&str) -> *const std::ffi::c_void,
|
||||
{
|
||||
self.display_realized_with(|api| api.display_realized(get_proc_address))
|
||||
}
|
||||
|
||||
/// Read back the pixels (RGBA8, bottom-up like `glReadPixels`) of the
|
||||
/// most recently presented frame. Intended for tests and screenshots;
|
||||
/// requires the GL context to be current.
|
||||
pub fn last_frame_pixels(&self) -> Option<(usize, usize, Vec<u8>)> {
|
||||
let d = lock(&self.draw);
|
||||
let target = Arc::clone(d.api.last_target()?);
|
||||
let target = d.api.last_target()?;
|
||||
let gl = d.api.gl()?.clone();
|
||||
let bind = gl.bind_framebuffer(target.framebuffer(), gl::FramebufferTarget::Read);
|
||||
let bind = gl.bind_framebuffer(
|
||||
target.framebuffer(),
|
||||
crate::opengl::gl::FramebufferTarget::Read,
|
||||
);
|
||||
let px = gl.read_pixels_rgba8(0, 0, target.width as i32, target.height as i32);
|
||||
gl.unbind_framebuffer(bind);
|
||||
Some((target.width, target.height, px))
|
||||
}
|
||||
|
||||
// Callback from the graphics API when a frame is completed.
|
||||
fn frame_completed(&self, health: Health) {
|
||||
// If our health value hasn't changed, then we do nothing. We don't
|
||||
// do a cmpxchg here because strict atomicity isn't important.
|
||||
if self.health.load(Ordering::SeqCst) != health as u8 {
|
||||
self.health.store(health as u8, Ordering::SeqCst);
|
||||
|
||||
// Our health value changed, so we notify the surface so that it
|
||||
// can do something about it.
|
||||
self.host.renderer_health(health);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1646,7 +1844,7 @@ struct Critical {
|
||||
/// (~12 minutes at 120Hz).
|
||||
const MAX_TERMINAL_STATE_FRAME_COUNT: usize = 100_000;
|
||||
|
||||
impl Renderer {
|
||||
impl<G: GraphicsApi> Renderer<G> {
|
||||
/// Update the frame data.
|
||||
///
|
||||
/// This is the renderer thread half of the renderer: it locks
|
||||
@@ -1849,8 +2047,16 @@ impl Renderer {
|
||||
(t.config.background_opacity * 255.0).round() as u8,
|
||||
];
|
||||
|
||||
// (On macOS glass styles the background opacity is removed; that
|
||||
// is out of scope on Linux.)
|
||||
// If we're on macOS and have glass styles, we remove
|
||||
// the background opacity because the glass effect handles
|
||||
// it.
|
||||
#[cfg(target_os = "macos")]
|
||||
if matches!(
|
||||
t.config.background_blur,
|
||||
BackgroundBlur::MacosGlassRegular | BackgroundBlur::MacosGlassClear
|
||||
) {
|
||||
d.uniforms.bg_color[3] = 0;
|
||||
}
|
||||
|
||||
// Prepare our overlay image for upload (or unload).
|
||||
d.has_overlay = t.overlay.is_some();
|
||||
@@ -1922,8 +2128,8 @@ impl Renderer {
|
||||
|
||||
/// Update the kitty image state from the terminal (the `kittyUpdate` call in
|
||||
/// `updateFrame`).
|
||||
fn update_kitty(
|
||||
images: &mut image::State,
|
||||
fn update_kitty<G: GraphicsApi>(
|
||||
images: &mut image::State<G>,
|
||||
terminal: &mut ghostty_terminal::terminal::Terminal,
|
||||
cell_size: CellSize,
|
||||
) {
|
||||
@@ -1955,7 +2161,7 @@ enum Selected {
|
||||
SearchSelected,
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
impl<G: GraphicsApi> DrawState<G> {
|
||||
/// Convert the terminal state to GPU cells stored in CPU memory. These
|
||||
/// are then synced to the GPU in the next frame. This only updates CPU
|
||||
/// memory and doesn't touch the GPU.
|
||||
@@ -2246,7 +2452,7 @@ impl DrawState {
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
impl<G: GraphicsApi> DrawState<G> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rebuild_row(
|
||||
&mut self,
|
||||
@@ -2968,7 +3174,7 @@ impl DrawState {
|
||||
// ---------------------------------------------------------------------------
|
||||
// draw_frame (GL thread)
|
||||
|
||||
impl Renderer {
|
||||
impl<G: GraphicsApi> Renderer<G> {
|
||||
/// Draw the frame to the screen.
|
||||
///
|
||||
/// If `sync` is true, this will synchronously block until
|
||||
@@ -3000,7 +3206,7 @@ impl Renderer {
|
||||
result
|
||||
}
|
||||
|
||||
fn draw_frame_locked(&self, d: &mut DrawState, sync: bool) -> Result<(), Error> {
|
||||
fn draw_frame_locked(&self, d: &mut DrawState<G>, sync: bool) -> Result<(), Error> {
|
||||
d.api.draw_frame_start();
|
||||
|
||||
// Retrieve the most up-to-date surface size from the Graphics API
|
||||
@@ -3030,30 +3236,45 @@ impl Renderer {
|
||||
|
||||
// Get a frame state to draw to.
|
||||
let (idx, mut frame) = d.swap_chain.next_frame()?;
|
||||
let result = d.draw_with_frame(
|
||||
&mut frame,
|
||||
sync,
|
||||
(surface_width, surface_height),
|
||||
size_changed,
|
||||
);
|
||||
d.swap_chain.release_frame(idx, frame);
|
||||
|
||||
let health = result?;
|
||||
let result = d.draw_with_frame(&mut frame, (surface_width, surface_height), size_changed);
|
||||
let frame_ctx = match result {
|
||||
Ok(frame_ctx) => frame_ctx,
|
||||
Err(err) => {
|
||||
// Nothing was submitted: give the frame state back.
|
||||
d.swap_chain.release_frame(idx, frame);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
self.has_animations
|
||||
.store(d.has_custom_shaders, Ordering::Relaxed);
|
||||
self.frame_completed(health);
|
||||
|
||||
// Complete the frame. The frame state is handed back to the swap
|
||||
// chain, and the health reported, when the frame completes
|
||||
// (`frameCompleted`): inline for synchronous frames, from the
|
||||
// graphics API's completion thread otherwise.
|
||||
let report = self.frame_completion();
|
||||
let swap_chain = d.swap_chain.releaser();
|
||||
let pending = Mutex::new(Some(frame));
|
||||
let completion: FrameCompletion = Arc::new(move |health| {
|
||||
report(health);
|
||||
if let Some(frame) = lock(&pending).take() {
|
||||
SwapChainInner::release(&swap_chain, idx, frame);
|
||||
}
|
||||
});
|
||||
frame_ctx.complete(sync, completion);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DrawState {
|
||||
impl<G: GraphicsApi> DrawState<G> {
|
||||
/// Encode a frame into `frame`'s buffers and the swap chain's targets.
|
||||
/// The returned graphics API frame has not been completed yet.
|
||||
fn draw_with_frame(
|
||||
&mut self,
|
||||
frame: &mut FrameState,
|
||||
sync: bool,
|
||||
frame: &mut FrameState<G>,
|
||||
surface_size: (u32, u32),
|
||||
size_changed: bool,
|
||||
) -> Result<Health, Error> {
|
||||
) -> Result<G::Frame, Error> {
|
||||
let (surface_width, surface_height) = surface_size;
|
||||
|
||||
// If we need to reinitialize our shaders, do so.
|
||||
@@ -3073,7 +3294,7 @@ impl DrawState {
|
||||
// if we have a state but don't need it we remove it.
|
||||
if self.has_custom_shaders {
|
||||
if frame.custom_shader_state.is_none() {
|
||||
let mut state = CustomShaderState::new(&self.api)?;
|
||||
let mut state = CustomShaderState::<G>::new(&self.api)?;
|
||||
state.resize(&self.api, surface_width as usize, surface_height as usize)?;
|
||||
frame.custom_shader_state = Some(state);
|
||||
}
|
||||
@@ -3094,8 +3315,8 @@ impl DrawState {
|
||||
// If this frame's target isn't the correct size, or the target
|
||||
// config has changed (such as when the blending mode changes),
|
||||
// remove it and replace it with a new one with the right values.
|
||||
if frame.target.width != self.size.screen.width as usize
|
||||
|| frame.target.height != self.size.screen.height as usize
|
||||
if frame.target.width() != self.size.screen.width as usize
|
||||
|| frame.target.height() != self.size.screen.height as usize
|
||||
|| frame.target_config_modified != self.target_config_modified
|
||||
{
|
||||
frame.resize(
|
||||
@@ -3108,26 +3329,21 @@ impl DrawState {
|
||||
self.api.clear_last_target();
|
||||
}
|
||||
|
||||
let gl: Gl = self.api.gl().ok_or(opengl::Error::NoContext)?.clone();
|
||||
let gpu = Gpu {
|
||||
gl: &gl,
|
||||
texture_options: self
|
||||
.api
|
||||
.image_texture_options(opengl::ImageTextureFormat::Rgba, true),
|
||||
buffer_options: self.api.image_buffer_options(),
|
||||
};
|
||||
{
|
||||
let gpu = Gpu { api: &self.api };
|
||||
|
||||
// Upload images to the GPU as necessary.
|
||||
let _ = self.images.upload(&gpu);
|
||||
// Upload images to the GPU as necessary.
|
||||
let _ = self.images.upload(&gpu);
|
||||
|
||||
// Upload the background image to the GPU as necessary.
|
||||
self.upload_background_image(&gpu);
|
||||
// Upload the background image to the GPU as necessary.
|
||||
upload_background_image(&mut self.bg_image, &gpu);
|
||||
}
|
||||
|
||||
// Update per-frame custom shader uniforms.
|
||||
self.update_custom_shader_uniforms_for_frame();
|
||||
|
||||
// Setup our frame data
|
||||
frame.uniforms.sync(&[self.uniforms])?;
|
||||
frame.uniforms.sync(&[G::gpu_uniforms(&self.uniforms)])?;
|
||||
frame.cells_bg.sync(&self.cells.bg_cells)?;
|
||||
let fg_lists: Vec<&[CellText]> = self.cells.fg_rows.iter().map(Vec::as_slice).collect();
|
||||
let fg_count = frame.cells.sync_from_slices(&fg_lists)?;
|
||||
@@ -3174,17 +3390,18 @@ impl DrawState {
|
||||
bg_image,
|
||||
has_overlay,
|
||||
..
|
||||
} = self;
|
||||
} = &*self;
|
||||
let Some(shaders) = shaders.as_ref() else {
|
||||
return Err(Error::Defunct);
|
||||
};
|
||||
let gpu = Gpu { api };
|
||||
let frame_ctx = api.begin_frame(Arc::clone(&frame.target));
|
||||
|
||||
{
|
||||
let attachments = [Attachment {
|
||||
target: match &frame.custom_shader_state {
|
||||
Some(state) => AttachmentTarget::Texture(&state.back_texture),
|
||||
None => AttachmentTarget::Target(&frame.target),
|
||||
None => AttachmentTarget::Target(&*frame.target),
|
||||
},
|
||||
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
|
||||
}];
|
||||
@@ -3204,7 +3421,7 @@ impl DrawState {
|
||||
Some(img) => {
|
||||
if let image::Image::Ready(texture) = img {
|
||||
pass.step(&Step {
|
||||
pipeline: &shaders.bg_image,
|
||||
pipeline: shaders.bg_image(),
|
||||
uniforms: Some(frame.uniforms.raw()),
|
||||
buffers: &[Some(frame.bg_image_buffer.raw())],
|
||||
textures: &[Some(texture)],
|
||||
@@ -3215,7 +3432,7 @@ impl DrawState {
|
||||
}
|
||||
None => {
|
||||
pass.step(&Step {
|
||||
pipeline: &shaders.bg_color,
|
||||
pipeline: shaders.bg_color(),
|
||||
uniforms: Some(frame.uniforms.raw()),
|
||||
buffers: &[None, Some(frame.cells_bg.raw())],
|
||||
textures: &[],
|
||||
@@ -3229,14 +3446,14 @@ impl DrawState {
|
||||
// to be behind text AND cell backgrounds.
|
||||
images.draw(
|
||||
&gpu,
|
||||
&shaders.image,
|
||||
shaders.image(),
|
||||
&mut pass,
|
||||
image::DrawPlacements::KittyBelowBg,
|
||||
);
|
||||
|
||||
// Then we draw any opaque cell backgrounds.
|
||||
pass.step(&Step {
|
||||
pipeline: &shaders.cell_bg,
|
||||
pipeline: shaders.cell_bg(),
|
||||
uniforms: Some(frame.uniforms.raw()),
|
||||
buffers: &[None, Some(frame.cells_bg.raw())],
|
||||
textures: &[],
|
||||
@@ -3247,14 +3464,14 @@ impl DrawState {
|
||||
// Kitty images between cell backgrounds and text.
|
||||
images.draw(
|
||||
&gpu,
|
||||
&shaders.image,
|
||||
shaders.image(),
|
||||
&mut pass,
|
||||
image::DrawPlacements::KittyBelowText,
|
||||
);
|
||||
|
||||
// Text.
|
||||
pass.step(&Step {
|
||||
pipeline: &shaders.cell_text,
|
||||
pipeline: shaders.cell_text(),
|
||||
uniforms: Some(frame.uniforms.raw()),
|
||||
buffers: &[Some(frame.cells.raw()), Some(frame.cells_bg.raw())],
|
||||
textures: &[Some(&frame.grayscale), Some(&frame.color)],
|
||||
@@ -3269,7 +3486,7 @@ impl DrawState {
|
||||
// Kitty images in front of text.
|
||||
images.draw(
|
||||
&gpu,
|
||||
&shaders.image,
|
||||
shaders.image(),
|
||||
&mut pass,
|
||||
image::DrawPlacements::KittyAboveText,
|
||||
);
|
||||
@@ -3279,7 +3496,7 @@ impl DrawState {
|
||||
if *has_overlay {
|
||||
images.draw(
|
||||
&gpu,
|
||||
&shaders.image,
|
||||
shaders.image(),
|
||||
&mut pass,
|
||||
image::DrawPlacements::Overlay,
|
||||
);
|
||||
@@ -3290,14 +3507,14 @@ impl DrawState {
|
||||
|
||||
// If we have custom shaders, then we render them.
|
||||
if let Some(state) = &mut frame.custom_shader_state {
|
||||
let post_count = shaders.post_pipelines.len();
|
||||
for (i, pipeline) in shaders.post_pipelines.iter().enumerate() {
|
||||
let post_count = shaders.post_pipelines().len();
|
||||
for (i, pipeline) in shaders.post_pipelines().iter().enumerate() {
|
||||
{
|
||||
let attachments = [Attachment {
|
||||
target: if i < post_count - 1 {
|
||||
AttachmentTarget::Texture(&state.front_texture)
|
||||
} else {
|
||||
AttachmentTarget::Target(&frame.target)
|
||||
AttachmentTarget::Target(&*frame.target)
|
||||
},
|
||||
clear_color: Some([0.0, 0.0, 0.0, 0.0]),
|
||||
}];
|
||||
@@ -3317,8 +3534,8 @@ impl DrawState {
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the frame: glFinish, error check, present.
|
||||
Ok(frame_ctx.complete(sync))
|
||||
// The caller completes the frame (glFinish, error check, present).
|
||||
Ok(frame_ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3433,13 +3650,8 @@ mod tests {
|
||||
std::fs::write(&path, &bytes).unwrap();
|
||||
|
||||
let img = load_background_image(path.to_str().unwrap()).expect("decoded");
|
||||
match img {
|
||||
image::Image::Pending(p) => {
|
||||
assert_eq!((p.width, p.height), (2, 1));
|
||||
assert_eq!(p.data, [1, 2, 3, 255, 4, 5, 6, 128]);
|
||||
}
|
||||
_ => panic!("expected a pending image"),
|
||||
}
|
||||
assert_eq!((img.width, img.height), (2, 1));
|
||||
assert_eq!(img.data, [1, 2, 3, 255, 4, 5, 6, 128]);
|
||||
|
||||
// Missing / undecodable files are not an error, just no image.
|
||||
assert!(load_background_image(dir.join("missing.png").to_str().unwrap()).is_none());
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
//! The graphics API abstraction used by the generic renderer.
|
||||
//!
|
||||
//! Zig's `GenericRenderer(comptime GraphicsAPI: type)` duck-types its graphics
|
||||
//! API (`Metal`, `OpenGL`): it reads associated declarations such as
|
||||
//! `GraphicsAPI.Target`, `GraphicsAPI.Buffer`, `GraphicsAPI.shaders`,
|
||||
//! `GraphicsAPI.swap_chain_count`, `custom_shader_target`,
|
||||
//! `custom_shader_y_is_down`, and calls methods like `initTarget`,
|
||||
//! `beginFrame`, `drawFrameStart`. [`GraphicsApi`] is the Rust spelling of that
|
||||
//! implicit interface: associated types for the GPU objects, associated
|
||||
//! consts for the per-API constants, and methods for the operations.
|
||||
//!
|
||||
//! `renderer.zig` picks the implementation by `build_config.renderer`
|
||||
//! (`.metal` => `GenericRenderer(Metal)`, `.opengl` => `GenericRenderer(OpenGL)`);
|
||||
//! `lib.rs` does the same with `cfg(target_vendor = "apple")`.
|
||||
|
||||
use std::{error::Error as StdError, fmt, sync::Arc};
|
||||
|
||||
use bytemuck::Pod;
|
||||
use ghostty_config::types_b::AlphaBlending;
|
||||
use ghostty_font::atlas::Atlas;
|
||||
|
||||
use crate::{backend::Health, shader_types::Uniforms, shadertoy};
|
||||
|
||||
/// A graphics API failure (OpenGL error, Metal failure, ...). The generic
|
||||
/// renderer only reports these, so the concrete error is boxed.
|
||||
#[derive(Debug)]
|
||||
pub struct ApiError(Box<dyn StdError + Send + Sync>);
|
||||
|
||||
impl ApiError {
|
||||
/// Wrap a concrete error.
|
||||
pub fn new<E: StdError + Send + Sync + 'static>(err: E) -> Self {
|
||||
ApiError(Box::new(err))
|
||||
}
|
||||
|
||||
/// An error that is only a message.
|
||||
pub fn msg(msg: impl Into<String>) -> Self {
|
||||
ApiError(msg.into().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ApiError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for ApiError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.0.source()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called with the health of a frame once it has completed. For synchronous
|
||||
/// APIs (OpenGL, or Metal with `sync = true`) this happens inside
|
||||
/// [`GpuFrame::complete`]; for asynchronous Metal frames it happens on the
|
||||
/// command buffer's completion thread (Zig: `renderer.frameCompleted`).
|
||||
pub type FrameCompletion = Arc<dyn Fn(Health) + Send + Sync>;
|
||||
|
||||
/// Primitive types we support drawing (same subset in OpenGL and Metal).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Primitive {
|
||||
Point,
|
||||
Line,
|
||||
LineStrip,
|
||||
Triangle,
|
||||
TriangleStrip,
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImageTextureFormat {
|
||||
/// 1 byte per pixel grayscale.
|
||||
Gray,
|
||||
/// 4 bytes per pixel RGBA.
|
||||
Rgba,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
Bgra,
|
||||
}
|
||||
|
||||
/// Which texture options (`textureOptions` / `imageTextureOptions`) to create
|
||||
/// a texture with.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TextureKind {
|
||||
/// `api.textureOptions()`: the front/back textures of custom shaders
|
||||
/// (shader read + render target).
|
||||
CustomShader,
|
||||
/// `api.imageTextureOptions(format, srgb)`: shader-read-only images.
|
||||
Image {
|
||||
format: ImageTextureFormat,
|
||||
srgb: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// What a color attachment renders into.
|
||||
pub enum AttachmentTarget<'a, G: GraphicsApi> {
|
||||
Texture(&'a G::Texture),
|
||||
Target(&'a G::Target),
|
||||
}
|
||||
|
||||
/// Describes a color attachment.
|
||||
pub struct Attachment<'a, G: GraphicsApi> {
|
||||
pub target: AttachmentTarget<'a, G>,
|
||||
pub clear_color: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
/// Describes the draw call for a step.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Draw {
|
||||
pub ty: Primitive,
|
||||
pub vertex_count: usize,
|
||||
pub instance_count: usize,
|
||||
}
|
||||
|
||||
impl Draw {
|
||||
pub fn new(ty: Primitive, vertex_count: usize) -> Self {
|
||||
Draw {
|
||||
ty,
|
||||
vertex_count,
|
||||
instance_count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a step in a render pass.
|
||||
///
|
||||
/// Buffer slot conventions are shared by both APIs: `uniforms` is bound at
|
||||
/// index 1, `buffers[0]` is the vertex buffer, the rest are storage buffers
|
||||
/// bound at their slice index; `textures` / `samplers` bind at their slice
|
||||
/// index.
|
||||
pub struct Step<'a, G: GraphicsApi> {
|
||||
pub pipeline: &'a G::Pipeline,
|
||||
pub uniforms: Option<G::BufferRef>,
|
||||
pub buffers: &'a [Option<G::BufferRef>],
|
||||
pub textures: &'a [Option<&'a G::Texture>],
|
||||
pub samplers: &'a [Option<&'a G::Sampler>],
|
||||
pub draw: Draw,
|
||||
}
|
||||
|
||||
/// A GPU buffer holding `T`s (`GraphicsAPI.Buffer(T)`).
|
||||
pub trait GpuBuffer<G: GraphicsApi, T: Pod> {
|
||||
/// The raw handle passed to render pass steps.
|
||||
fn raw(&self) -> G::BufferRef;
|
||||
|
||||
/// Sync new contents to the buffer. The data is expected to be the
|
||||
/// complete contents of the buffer. If the amount of data is larger
|
||||
/// than the buffer length, the buffer will be reallocated.
|
||||
fn sync(&mut self, data: &[T]) -> Result<(), ApiError>;
|
||||
|
||||
/// Like `sync` but takes data from several slices. Returns the number
|
||||
/// of items synced.
|
||||
fn sync_from_slices(&mut self, lists: &[&[T]]) -> Result<usize, ApiError>;
|
||||
}
|
||||
|
||||
/// A GPU texture (`GraphicsAPI.Texture`).
|
||||
pub trait GpuTexture {
|
||||
fn width(&self) -> usize;
|
||||
fn height(&self) -> usize;
|
||||
|
||||
/// Replace a region of the texture with the provided data. Does NOT
|
||||
/// check the dimensions of the data to ensure correctness.
|
||||
fn replace_region(
|
||||
&self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: &[u8],
|
||||
) -> Result<(), ApiError>;
|
||||
}
|
||||
|
||||
/// A render target that can be presented (`GraphicsAPI.Target`).
|
||||
pub trait GpuTarget {
|
||||
fn width(&self) -> usize;
|
||||
fn height(&self) -> usize;
|
||||
}
|
||||
|
||||
/// The collection of shader pipelines (`GraphicsAPI.shaders.Shaders`).
|
||||
pub trait GpuShaders<G: GraphicsApi> {
|
||||
fn bg_color(&self) -> &G::Pipeline;
|
||||
fn cell_bg(&self) -> &G::Pipeline;
|
||||
fn cell_text(&self) -> &G::Pipeline;
|
||||
fn image(&self) -> &G::Pipeline;
|
||||
fn bg_image(&self) -> &G::Pipeline;
|
||||
|
||||
/// Custom shaders to run against the final drawable texture, each one
|
||||
/// against the output of the previous.
|
||||
fn post_pipelines(&self) -> &[G::Pipeline];
|
||||
}
|
||||
|
||||
/// A frame being encoded (`GraphicsAPI.Frame`).
|
||||
pub trait GpuFrame<G: GraphicsApi> {
|
||||
/// Add a render pass to this frame with the provided attachments.
|
||||
fn render_pass<'a>(&'a self, attachments: &'a [Attachment<'a, G>]) -> G::RenderPass<'a>
|
||||
where
|
||||
G: 'a;
|
||||
|
||||
/// Complete this frame and present the target. If `sync` is true, this
|
||||
/// blocks until the frame is presented. The frame's health is reported
|
||||
/// through `completion` (before this returns if the frame completes
|
||||
/// synchronously).
|
||||
fn complete(self, sync: bool, completion: FrameCompletion);
|
||||
}
|
||||
|
||||
/// A render pass being encoded (`GraphicsAPI.RenderPass`).
|
||||
pub trait GpuRenderPass<'a, G: GraphicsApi> {
|
||||
/// Add a step to this render pass.
|
||||
fn step(&mut self, step: &Step<'_, G>);
|
||||
|
||||
/// Complete this render pass. It can no longer be used after this.
|
||||
fn complete(self);
|
||||
}
|
||||
|
||||
/// The graphics API the generic renderer is instantiated with.
|
||||
pub trait GraphicsApi: Sized + Send + 'static {
|
||||
/// `custom_shader_target`: Metal => MSL, OpenGL => GLSL.
|
||||
const CUSTOM_SHADER_TARGET: shadertoy::Target;
|
||||
/// `custom_shader_y_is_down`: the fragCoord for Metal shaders is +Y = down.
|
||||
const CUSTOM_SHADER_Y_IS_DOWN: bool;
|
||||
/// `swap_chain_count`: Metal triple buffers, OpenGL completes frames
|
||||
/// synchronously and needs no multi-buffering.
|
||||
const SWAP_CHAIN_COUNT: usize;
|
||||
|
||||
type Target: GpuTarget + Send + Sync + 'static;
|
||||
type Texture: GpuTexture + Send + 'static;
|
||||
type Sampler: Send + 'static;
|
||||
type Buffer<T: Pod>: GpuBuffer<Self, T> + Send + 'static;
|
||||
/// The raw buffer handle bound by render pass steps.
|
||||
type BufferRef: Clone;
|
||||
type Pipeline;
|
||||
type Shaders: GpuShaders<Self> + Send + 'static;
|
||||
/// The GPU-side layout of the uniform block (`shaders.Uniforms`).
|
||||
type GpuUniforms: Pod;
|
||||
type Frame: GpuFrame<Self>;
|
||||
type RenderPass<'a>: GpuRenderPass<'a, Self>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
/// Convert the renderer's CPU-side uniforms to the GPU layout.
|
||||
fn gpu_uniforms(uniforms: &Uniforms) -> Self::GpuUniforms;
|
||||
|
||||
/// The current alpha blending mode.
|
||||
fn blending(&self) -> AlphaBlending;
|
||||
|
||||
/// Change the alpha blending mode (`api.blending = ..`). Targets and
|
||||
/// textures must be recreated by the caller afterwards.
|
||||
fn set_blending(&mut self, blending: AlphaBlending);
|
||||
|
||||
/// Build the shader pipelines, including the post-processing pipelines
|
||||
/// for `custom_shaders` (in [`Self::CUSTOM_SHADER_TARGET`] format).
|
||||
fn init_shaders(&self, custom_shaders: &[String]) -> Result<Self::Shaders, ApiError>;
|
||||
|
||||
/// Get the current size of the runtime surface.
|
||||
fn surface_size(&self) -> Result<(u32, u32), ApiError>;
|
||||
|
||||
/// Initialize a new render target which can be presented by this API.
|
||||
fn init_target(&self, width: usize, height: usize) -> Result<Self::Target, ApiError>;
|
||||
|
||||
/// `Buffer.init` with the API's buffer options.
|
||||
fn init_buffer<T: Pod>(&self, len: usize) -> Result<Self::Buffer<T>, ApiError>;
|
||||
|
||||
/// `Buffer.initFill` with the API's buffer options.
|
||||
fn init_buffer_fill<T: Pod>(&self, data: &[T]) -> Result<Self::Buffer<T>, ApiError>;
|
||||
|
||||
/// Initialize a texture with the options of `kind`.
|
||||
fn init_texture(
|
||||
&self,
|
||||
kind: TextureKind,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Option<&[u8]>,
|
||||
) -> Result<Self::Texture, ApiError>;
|
||||
|
||||
/// Initializes a Texture suitable for the provided font atlas.
|
||||
fn init_atlas_texture(&self, atlas: &Atlas) -> Result<Self::Texture, ApiError>;
|
||||
|
||||
/// Initialize the sampler used by custom shaders (`samplerOptions`).
|
||||
fn init_sampler(&self) -> Result<Self::Sampler, ApiError>;
|
||||
|
||||
/// Begin a frame. The target is presented once the frame is completed
|
||||
/// ([`GpuFrame::complete`]).
|
||||
fn begin_frame(&self, target: Arc<Self::Target>) -> Self::Frame;
|
||||
|
||||
/// Present the last presented target again (a no-op for Metal).
|
||||
fn present_last_target(&self) -> Result<(), ApiError>;
|
||||
|
||||
/// Forget the remembered last-presented target (it is being replaced).
|
||||
fn clear_last_target(&self);
|
||||
|
||||
/// Actions taken before doing anything in `drawFrame` (Metal starts an
|
||||
/// autorelease pool).
|
||||
fn draw_frame_start(&mut self) {}
|
||||
|
||||
/// Actions taken after `drawFrame` is done.
|
||||
fn draw_frame_end(&mut self) {}
|
||||
|
||||
/// Called by the renderer thread when it begins (`threadEnter`).
|
||||
fn thread_enter(&self) {}
|
||||
|
||||
/// Called by the renderer thread when it exits (`threadExit`).
|
||||
fn thread_exit(&self) {}
|
||||
|
||||
/// Called when the renderer thread starts its main loop (`loopEnter`).
|
||||
/// `draw` draws a frame synchronously; Metal installs it as the layer's
|
||||
/// display callback.
|
||||
fn loop_enter(&mut self, draw: Arc<dyn Fn() + Send + Sync>) {
|
||||
let _ = draw;
|
||||
}
|
||||
|
||||
/// Called when the renderer thread leaves its main loop (`loopExit`).
|
||||
fn loop_exit(&mut self) {}
|
||||
|
||||
/// Called when the display is unrealized (`displayUnrealized`).
|
||||
fn display_unrealized(&mut self) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn api_error_display() {
|
||||
let e = ApiError::msg("boom");
|
||||
assert_eq!(e.to_string(), "boom");
|
||||
let e = ApiError::new(std::io::Error::other("io"));
|
||||
assert_eq!(e.to_string(), "io");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_defaults_to_one_instance() {
|
||||
let d = Draw::new(Primitive::Triangle, 3);
|
||||
assert_eq!(d.instance_count, 1);
|
||||
assert_eq!(d.vertex_count, 3);
|
||||
}
|
||||
}
|
||||
@@ -21,39 +21,34 @@ use ghostty_terminal::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
opengl::{
|
||||
Buffer,
|
||||
Texture,
|
||||
buffer,
|
||||
gl::{Gl, Primitive},
|
||||
pipeline::Pipeline,
|
||||
render_pass::{Draw, RenderPass, Step},
|
||||
shaders::Image as ImageVertex,
|
||||
texture,
|
||||
graphics_api::{
|
||||
Draw,
|
||||
GpuBuffer,
|
||||
GpuRenderPass,
|
||||
GraphicsApi,
|
||||
ImageTextureFormat,
|
||||
Primitive,
|
||||
Step,
|
||||
TextureKind,
|
||||
},
|
||||
overlay::Overlay,
|
||||
shader_types::Image as ImageVertex,
|
||||
size::CellSize,
|
||||
};
|
||||
|
||||
/// The GPU objects needed to upload and draw images. Built per frame from
|
||||
/// the graphics API (the API itself is mutably borrowed by the frame while
|
||||
/// drawing).
|
||||
pub struct Gpu<'a> {
|
||||
pub gl: &'a Gl,
|
||||
/// `api.imageTextureOptions(.rgba, true)`.
|
||||
pub texture_options: texture::Options,
|
||||
/// `api.imageBufferOptions()`.
|
||||
pub buffer_options: buffer::Options,
|
||||
/// the graphics API.
|
||||
pub struct Gpu<'a, G: GraphicsApi> {
|
||||
pub api: &'a G,
|
||||
}
|
||||
|
||||
/// Generic image rendering state for the renderer. This stores all
|
||||
/// images and their placements and exposes only a limited public API
|
||||
/// for adding images and placements and drawing them.
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
pub struct State<G: GraphicsApi> {
|
||||
/// The full image state for the renderer that specifies what images
|
||||
/// need to be uploaded, pruned, etc.
|
||||
pub images: HashMap<Id, Entry>,
|
||||
pub images: HashMap<Id, Entry<G>>,
|
||||
|
||||
/// The placements for the Kitty image protocol.
|
||||
pub kitty_placements: Vec<Placement>,
|
||||
@@ -77,8 +72,8 @@ pub struct State {
|
||||
}
|
||||
|
||||
/// An image in the map plus the time it was transmitted.
|
||||
pub struct Entry {
|
||||
pub image: Image,
|
||||
pub struct Entry<G: GraphicsApi> {
|
||||
pub image: Image<G>,
|
||||
pub transmit_time: Instant,
|
||||
}
|
||||
|
||||
@@ -96,8 +91,22 @@ pub enum PrepImageError {
|
||||
ImageConversionError,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> State {
|
||||
impl<G: GraphicsApi> Default for State<G> {
|
||||
fn default() -> Self {
|
||||
State {
|
||||
images: HashMap::new(),
|
||||
kitty_placements: Vec::new(),
|
||||
kitty_bg_end: 0,
|
||||
kitty_text_end: 0,
|
||||
kitty_virtual: false,
|
||||
overlay_placements: Vec::new(),
|
||||
force_kitty_update: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<G: GraphicsApi> State<G> {
|
||||
pub fn new() -> State<G> {
|
||||
State::default()
|
||||
}
|
||||
|
||||
@@ -107,7 +116,7 @@ impl State {
|
||||
/// If any uploads fail, they are ignored. The return value
|
||||
/// can be used to detect if upload was a total success (true)
|
||||
/// or not (false).
|
||||
pub fn upload(&mut self, gpu: &Gpu<'_>) -> bool {
|
||||
pub fn upload(&mut self, gpu: &Gpu<'_, G>) -> bool {
|
||||
let mut success = true;
|
||||
self.images.retain(|_, entry| {
|
||||
let img = &mut entry.image;
|
||||
@@ -131,11 +140,11 @@ impl State {
|
||||
///
|
||||
/// Any placements that have non-uploaded images are ignored. Any
|
||||
/// graphics API errors during drawing are also ignored.
|
||||
pub fn draw(
|
||||
pub fn draw<'p, P: GpuRenderPass<'p, G>>(
|
||||
&self,
|
||||
gpu: &Gpu<'_>,
|
||||
pipeline: &Pipeline,
|
||||
pass: &mut RenderPass<'_>,
|
||||
gpu: &Gpu<'_, G>,
|
||||
pipeline: &G::Pipeline,
|
||||
pass: &mut P,
|
||||
placement_type: DrawPlacements,
|
||||
) {
|
||||
let bg_end = self.kitty_bg_end as usize;
|
||||
@@ -162,22 +171,18 @@ impl State {
|
||||
|
||||
// Create our vertex buffer, which is always exactly one item.
|
||||
// future(mitchellh): we can group rendering multiple instances of a single image
|
||||
let buf = match Buffer::<ImageVertex>::init_fill(
|
||||
gpu.gl,
|
||||
gpu.buffer_options,
|
||||
&[ImageVertex {
|
||||
grid_pos: [p.x as f32, p.y as f32],
|
||||
cell_offset: [p.cell_offset_x as f32, p.cell_offset_y as f32],
|
||||
source_rect: [
|
||||
p.source_x as f32,
|
||||
p.source_y as f32,
|
||||
p.source_width as f32,
|
||||
p.source_height as f32,
|
||||
],
|
||||
dest_size: [p.width as f32, p.height as f32],
|
||||
pad: [0.0; 2],
|
||||
}],
|
||||
) {
|
||||
let buf = match gpu.api.init_buffer_fill::<ImageVertex>(&[ImageVertex {
|
||||
grid_pos: [p.x as f32, p.y as f32],
|
||||
cell_offset: [p.cell_offset_x as f32, p.cell_offset_y as f32],
|
||||
source_rect: [
|
||||
p.source_x as f32,
|
||||
p.source_y as f32,
|
||||
p.source_width as f32,
|
||||
p.source_height as f32,
|
||||
],
|
||||
dest_size: [p.width as f32, p.height as f32],
|
||||
pad: [0.0; 2],
|
||||
}]) {
|
||||
Ok(b) => b,
|
||||
Err(err) => {
|
||||
log::warn!("error creating image vertex buffer err={err}");
|
||||
@@ -770,27 +775,33 @@ pub enum UploadError {
|
||||
}
|
||||
|
||||
/// The state for a single image that is to be rendered.
|
||||
pub enum Image {
|
||||
pub enum Image<G: GraphicsApi> {
|
||||
/// The image data is pending upload to the GPU.
|
||||
Pending(Pending),
|
||||
|
||||
/// This is the same as the pending states but there is
|
||||
/// a texture already allocated that we want to replace.
|
||||
Replace { texture: Texture, pending: Pending },
|
||||
Replace {
|
||||
texture: G::Texture,
|
||||
pending: Pending,
|
||||
},
|
||||
|
||||
/// The image is uploaded and ready to be used.
|
||||
Ready(Texture),
|
||||
Ready(G::Texture),
|
||||
|
||||
/// The image isn't uploaded yet but is scheduled to be unloaded.
|
||||
UnloadPending(Pending),
|
||||
/// The image is uploaded and is scheduled to be unloaded.
|
||||
UnloadReady(Texture),
|
||||
UnloadReady(G::Texture),
|
||||
/// The image is uploaded and scheduled to be replaced
|
||||
/// with new data, but it's also scheduled to be unloaded.
|
||||
UnloadReplace { texture: Texture, pending: Pending },
|
||||
UnloadReplace {
|
||||
texture: G::Texture,
|
||||
pending: Pending,
|
||||
},
|
||||
}
|
||||
|
||||
impl Image {
|
||||
impl<G: GraphicsApi> Image<G> {
|
||||
/// Mark this image for unload whatever state it is in.
|
||||
pub fn mark_for_unload(&mut self) {
|
||||
let old = std::mem::replace(self, Image::Pending(Pending::empty()));
|
||||
@@ -807,7 +818,7 @@ impl Image {
|
||||
/// Mark the current image to be replaced with a pending one. This will
|
||||
/// attempt to update the existing texture if we have one, otherwise it
|
||||
/// will act like a new upload.
|
||||
pub fn mark_for_replace(&mut self, img: Image) {
|
||||
pub fn mark_for_replace(&mut self, img: Image<G>) {
|
||||
let Image::Pending(new_pending) = img else {
|
||||
debug_assert!(false, "mark_for_replace requires a pending image");
|
||||
return;
|
||||
@@ -850,7 +861,7 @@ impl Image {
|
||||
}
|
||||
|
||||
/// The texture if the image is ready or unload_ready (usable for drawing).
|
||||
pub fn ready_texture(&self) -> Option<&Texture> {
|
||||
pub fn ready_texture(&self) -> Option<&G::Texture> {
|
||||
match self {
|
||||
Image::Ready(t) | Image::UnloadReady(t) => Some(t),
|
||||
_ => None,
|
||||
@@ -873,7 +884,7 @@ impl Image {
|
||||
}
|
||||
}
|
||||
|
||||
fn texture(&self) -> Option<&Texture> {
|
||||
fn texture(&self) -> Option<&G::Texture> {
|
||||
match self {
|
||||
Image::Ready(t) | Image::UnloadReady(t) => Some(t),
|
||||
Image::Replace { texture, .. } | Image::UnloadReplace { texture, .. } => Some(texture),
|
||||
@@ -894,7 +905,7 @@ impl Image {
|
||||
|
||||
/// Upload the pending image to the GPU and change the state of this
|
||||
/// image to ready.
|
||||
pub fn upload(&mut self, gpu: &Gpu<'_>) -> Result<(), UploadError> {
|
||||
pub fn upload(&mut self, gpu: &Gpu<'_, G>) -> Result<(), UploadError> {
|
||||
// No error recovery is required after this call because it just
|
||||
// converts in place and is idempotent.
|
||||
self.prep_for_upload()?;
|
||||
@@ -905,14 +916,19 @@ impl Image {
|
||||
};
|
||||
|
||||
// Create our texture
|
||||
let texture = Texture::init(
|
||||
gpu.gl,
|
||||
gpu.texture_options,
|
||||
p.width as usize,
|
||||
p.height as usize,
|
||||
Some(&p.data),
|
||||
)
|
||||
.map_err(|_| UploadError::UploadFailed)?;
|
||||
// (`api.imageTextureOptions(.rgba, true)`)
|
||||
let texture = gpu
|
||||
.api
|
||||
.init_texture(
|
||||
TextureKind::Image {
|
||||
format: ImageTextureFormat::Rgba,
|
||||
srgb: true,
|
||||
},
|
||||
p.width as usize,
|
||||
p.height as usize,
|
||||
Some(&p.data),
|
||||
)
|
||||
.map_err(|_| UploadError::UploadFailed)?;
|
||||
|
||||
// Uploaded. We can now clear our data and change our state.
|
||||
//
|
||||
@@ -938,6 +954,7 @@ impl Pending {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::opengl::OpenGL;
|
||||
|
||||
fn pending(fmt: PixelFormat, data: Vec<u8>, w: u32, h: u32) -> Pending {
|
||||
Pending {
|
||||
@@ -1001,7 +1018,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn state_transitions_without_gpu() {
|
||||
let mut img = Image::Pending(pending(PixelFormat::Rgb, vec![1, 2, 3], 1, 1));
|
||||
let mut img: Image<OpenGL> = Image::Pending(pending(PixelFormat::Rgb, vec![1, 2, 3], 1, 1));
|
||||
assert!(img.is_pending());
|
||||
assert!(!img.is_unloading());
|
||||
assert!(!img.has_texture());
|
||||
@@ -1028,7 +1045,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prep_image_same_transmit_time_is_noop() {
|
||||
let mut s = State::new();
|
||||
let mut s: State<OpenGL> = State::new();
|
||||
let t = Instant::now();
|
||||
s.prep_image(
|
||||
Id::Kitty(1),
|
||||
@@ -1118,7 +1135,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let info = TerminalInfo::from_terminal(&t);
|
||||
let mut s = State::new();
|
||||
let mut s: State<OpenGL> = State::new();
|
||||
s.kitty_update(
|
||||
&storage,
|
||||
&t.screen().pages,
|
||||
|
||||
@@ -14,16 +14,23 @@
|
||||
pub mod backend;
|
||||
pub mod cell;
|
||||
pub mod cursor;
|
||||
pub mod display_link;
|
||||
pub mod generic;
|
||||
pub mod graphics_api;
|
||||
pub mod headless;
|
||||
pub mod image;
|
||||
pub mod link;
|
||||
pub mod math;
|
||||
pub mod message;
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code, clippy::struct_field_names)]
|
||||
pub mod metal;
|
||||
pub mod metal_layout;
|
||||
pub mod opengl;
|
||||
pub mod options;
|
||||
pub mod overlay;
|
||||
pub mod row;
|
||||
pub mod shader_types;
|
||||
pub mod shadertoy;
|
||||
mod shadertoy_sampler;
|
||||
pub mod size;
|
||||
@@ -37,9 +44,19 @@ pub use opengl::OpenGL;
|
||||
pub use size::{CellSize, Coordinate, GridSize, Padding, ScreenSize, Size};
|
||||
pub use state::State;
|
||||
|
||||
/// The renderer used on Linux: the generic renderer instantiated with the
|
||||
/// OpenGL graphics API (`renderer.zig`: `pub const Renderer = GenericRenderer(OpenGL)`).
|
||||
pub type Renderer = generic::Renderer;
|
||||
/// The graphics API the renderer is built with (`renderer.zig`: `Renderer =
|
||||
/// switch (build_config.renderer) { .metal => GenericRenderer(Metal), .opengl
|
||||
/// => GenericRenderer(OpenGL) }`): Metal on Apple platforms, OpenGL elsewhere
|
||||
/// (see [`Backend::default_for_target`]).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
pub type Api = metal::Metal;
|
||||
/// The graphics API the renderer is built with: OpenGL.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
pub type Api = opengl::OpenGL;
|
||||
|
||||
/// The renderer: the generic renderer instantiated with the graphics API
|
||||
/// selected for the target ([`Api`]).
|
||||
pub type Renderer = generic::Renderer<Api>;
|
||||
|
||||
/// The renderer's derived configuration (`Renderer.DerivedConfig`).
|
||||
pub type DerivedConfig = generic::DerivedConfig;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//! Metal data storage for a certain set of equal types (`metal/buffer.zig`).
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bytemuck::Pod;
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_foundation::NSRange;
|
||||
use objc2_metal::{MTLBuffer, MTLDevice as _, MTLResourceOptions};
|
||||
|
||||
use super::{Device, Error};
|
||||
use crate::{
|
||||
graphics_api::{ApiError, GpuBuffer},
|
||||
metal_layout::{ResourceOptions, StorageMode},
|
||||
};
|
||||
|
||||
/// Options for initializing a buffer.
|
||||
#[derive(Clone)]
|
||||
pub struct Options {
|
||||
/// MTLDevice
|
||||
pub device: Device,
|
||||
pub resource_options: ResourceOptions,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
fn mtl_options(&self) -> MTLResourceOptions {
|
||||
MTLResourceOptions(self.resource_options.bits() as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Metal data storage for a certain set of equal types. This is usually
|
||||
/// used for vertex buffers, etc. This helpful wrapper makes it easy to
|
||||
/// prealloc, shrink, grow, sync, buffers with Metal.
|
||||
pub struct Buffer<T: Pod> {
|
||||
/// The options this buffer was initialized with.
|
||||
opts: Options,
|
||||
|
||||
/// The underlying MTLBuffer object.
|
||||
buffer: Retained<ProtocolObject<dyn MTLBuffer>>,
|
||||
|
||||
/// The allocated length of the buffer. Note that this is the number
|
||||
/// of `T`s not the size in bytes.
|
||||
pub len: usize,
|
||||
|
||||
_marker: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
// SAFETY: an MTLBuffer is safe to use from any thread; CPU writes through
|
||||
// `contents` are serialized by `&mut self` and by the swap chain, which never
|
||||
// hands a buffer to the CPU while the GPU is using it.
|
||||
unsafe impl<T: Pod> Send for Buffer<T> {}
|
||||
// SAFETY: see above; shared access only reads the handle.
|
||||
unsafe impl<T: Pod> Sync for Buffer<T> {}
|
||||
|
||||
impl<T: Pod> Buffer<T> {
|
||||
/// The underlying MTLBuffer.
|
||||
pub fn buffer(&self) -> &ProtocolObject<dyn MTLBuffer> {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// A retained handle to the underlying MTLBuffer (what render pass steps
|
||||
/// bind).
|
||||
pub fn raw(&self) -> Retained<ProtocolObject<dyn MTLBuffer>> {
|
||||
self.buffer.clone()
|
||||
}
|
||||
|
||||
/// Initialize a buffer with the given length pre-allocated.
|
||||
pub fn init(opts: Options, len: usize) -> Result<Self, Error> {
|
||||
let buffer = opts
|
||||
.device
|
||||
.newBufferWithLength_options(len * size_of::<T>(), opts.mtl_options())
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
Ok(Buffer {
|
||||
buffer,
|
||||
opts,
|
||||
len,
|
||||
_marker: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Init the buffer filled with the given data.
|
||||
pub fn init_fill(opts: Options, data: &[T]) -> Result<Self, Error> {
|
||||
let bytes: &[u8] = bytemuck::cast_slice(data);
|
||||
// newBufferWithBytes rejects a null pointer / zero length, so empty
|
||||
// data allocates an (empty) buffer of length 0 via the length path.
|
||||
let buffer = if bytes.is_empty() {
|
||||
opts.device
|
||||
.newBufferWithLength_options(0, opts.mtl_options())
|
||||
} else {
|
||||
let ptr = std::ptr::NonNull::from(bytes).cast();
|
||||
// SAFETY: `ptr` points to `bytes.len()` readable bytes.
|
||||
unsafe {
|
||||
opts.device
|
||||
.newBufferWithBytes_length_options(ptr, bytes.len(), opts.mtl_options())
|
||||
}
|
||||
}
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
Ok(Buffer {
|
||||
buffer,
|
||||
opts,
|
||||
len: data.len(),
|
||||
_marker: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sync new contents to the buffer. The data is expected to be the
|
||||
/// complete contents of the buffer. If the amount of data is larger
|
||||
/// than the buffer length, the buffer will be reallocated.
|
||||
///
|
||||
/// If the amount of data is smaller than the buffer length, the
|
||||
/// remaining data in the buffer is left untouched.
|
||||
pub fn sync(&mut self, data: &[T]) -> Result<(), Error> {
|
||||
self.sync_from_slices(&[data]).map(|_| ())
|
||||
}
|
||||
|
||||
/// Like `Buffer::sync` but takes data from an array of slices, rather
|
||||
/// than a single slice. Returns the number of items synced.
|
||||
pub fn sync_from_slices(&mut self, lists: &[&[T]]) -> Result<usize, Error> {
|
||||
let total_len: usize = lists.iter().map(|l| l.len()).sum();
|
||||
|
||||
// If we need more bytes than our buffer has, we need to reallocate.
|
||||
let req_bytes = total_len * size_of::<T>();
|
||||
let avail_bytes = self.buffer.length();
|
||||
if req_bytes > avail_bytes {
|
||||
// Allocate a new buffer with enough to hold double what we
|
||||
// require (the old buffer is released when it is replaced).
|
||||
//
|
||||
// NOTE: Zig multiplies this size by `@sizeOf(T)` a second time,
|
||||
// over-allocating; we allocate exactly double.
|
||||
let size = req_bytes * 2;
|
||||
self.buffer = self
|
||||
.opts
|
||||
.device
|
||||
.newBufferWithLength_options(size, self.opts.mtl_options())
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
self.len = total_len * 2;
|
||||
}
|
||||
|
||||
// We can fit within the buffer so we can just replace bytes.
|
||||
let dst = self.buffer.contents().as_ptr().cast::<u8>();
|
||||
let mut offset = 0;
|
||||
for list in lists {
|
||||
let src: &[u8] = bytemuck::cast_slice(list);
|
||||
// SAFETY: `dst` points to a live buffer of at least `req_bytes`
|
||||
// bytes (checked/reallocated above) and `offset + src.len()` never
|
||||
// exceeds `req_bytes`; the regions can't overlap because the
|
||||
// buffer is GPU memory owned by us.
|
||||
unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst.add(offset), src.len()) };
|
||||
offset += src.len();
|
||||
}
|
||||
|
||||
// If we're using the managed resource storage mode, then
|
||||
// we need to signal Metal to synchronize the buffer data.
|
||||
//
|
||||
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
|
||||
if self.opts.resource_options.storage_mode == StorageMode::Managed {
|
||||
self.buffer.didModifyRange(NSRange::new(0, req_bytes));
|
||||
}
|
||||
|
||||
Ok(total_len)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Pod> GpuBuffer<super::Metal, T> for Buffer<T> {
|
||||
fn raw(&self) -> Retained<ProtocolObject<dyn MTLBuffer>> {
|
||||
Buffer::raw(self)
|
||||
}
|
||||
|
||||
fn sync(&mut self, data: &[T]) -> Result<(), ApiError> {
|
||||
Buffer::sync(self, data).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn sync_from_slices(&mut self, lists: &[&[T]]) -> Result<usize, ApiError> {
|
||||
Buffer::sync_from_slices(self, lists).map_err(ApiError::new)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//! Wrapper for handling render frames (`metal/Frame.zig`).
|
||||
use std::{ptr::NonNull, sync::Arc};
|
||||
|
||||
use block2::RcBlock;
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_metal::{MTLCommandBuffer, MTLCommandBufferStatus, MTLCommandQueue};
|
||||
|
||||
use super::{IOSurfaceLayer, Metal, Queue, render_pass::RenderPass, target::Target};
|
||||
use crate::{
|
||||
backend::Health,
|
||||
graphics_api::{Attachment, FrameCompletion, GpuFrame},
|
||||
};
|
||||
|
||||
/// A frame being encoded: one MTLCommandBuffer.
|
||||
pub struct Frame {
|
||||
/// MTLCommandBuffer (`None` if the queue failed to create one, in which
|
||||
/// case the frame is a no-op that completes unhealthy).
|
||||
buffer: Option<Retained<ProtocolObject<dyn MTLCommandBuffer>>>,
|
||||
|
||||
/// The layer the target is presented on.
|
||||
layer: IOSurfaceLayer,
|
||||
|
||||
/// The target is presented via the layer when completed.
|
||||
target: Arc<Target>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// Begin encoding a frame.
|
||||
pub fn begin(queue: &Queue, layer: IOSurfaceLayer, target: Arc<Target>) -> Frame {
|
||||
let buffer = queue.commandBuffer();
|
||||
if buffer.is_none() {
|
||||
log::warn!("unable to create a Metal command buffer");
|
||||
}
|
||||
Frame {
|
||||
buffer,
|
||||
layer,
|
||||
target,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a render pass to this frame with the provided attachments.
|
||||
/// Returns a RenderPass which allows render steps to be added.
|
||||
pub fn render_pass(&self, attachments: &[Attachment<'_, Metal>]) -> RenderPass {
|
||||
RenderPass::begin(self.buffer.as_deref(), attachments)
|
||||
}
|
||||
|
||||
/// Complete this frame and present the target.
|
||||
///
|
||||
/// If `sync` is true, this will block until the frame is presented.
|
||||
pub fn complete(self, sync: bool, completion: FrameCompletion) {
|
||||
let Frame {
|
||||
buffer,
|
||||
layer,
|
||||
target,
|
||||
} = self;
|
||||
let Some(buffer) = buffer else {
|
||||
completion(Health::Unhealthy);
|
||||
return;
|
||||
};
|
||||
|
||||
// What happens when the command buffer has finished (`bufferCompleted`).
|
||||
let finish = move |buffer: &ProtocolObject<dyn MTLCommandBuffer>, sync: bool| {
|
||||
// Get our command buffer status to pass back to the generic renderer.
|
||||
let health = health_for_status(buffer.status());
|
||||
|
||||
// If the frame is healthy, present it.
|
||||
if health == Health::Healthy {
|
||||
if sync {
|
||||
layer.set_surface_sync(target.surface());
|
||||
} else {
|
||||
layer.set_surface(target.surface());
|
||||
}
|
||||
}
|
||||
|
||||
completion(health);
|
||||
};
|
||||
|
||||
if sync {
|
||||
// If we need to complete synchronously, we wait until the buffer
|
||||
// is completed and invoke the callback directly.
|
||||
buffer.commit();
|
||||
buffer.waitUntilCompleted();
|
||||
finish(&buffer, true);
|
||||
} else {
|
||||
// Otherwise we add our block as a completion handler. It is
|
||||
// copied when we add the handler, and then the copy is deallocated
|
||||
// by the objc runtime on success.
|
||||
let block = RcBlock::new(
|
||||
move |buffer: NonNull<ProtocolObject<dyn MTLCommandBuffer>>| {
|
||||
// SAFETY: Metal passes the (live) command buffer.
|
||||
finish(unsafe { buffer.as_ref() }, false);
|
||||
},
|
||||
);
|
||||
// SAFETY: the block is a valid completion handler and is copied
|
||||
// by `addCompletedHandler`.
|
||||
unsafe { buffer.addCompletedHandler(RcBlock::as_ptr(&block)) };
|
||||
buffer.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The health of a frame from its command buffer status.
|
||||
pub fn health_for_status(status: MTLCommandBufferStatus) -> Health {
|
||||
if status == MTLCommandBufferStatus::Error {
|
||||
Health::Unhealthy
|
||||
} else {
|
||||
Health::Healthy
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuFrame<Metal> for Frame {
|
||||
fn render_pass<'a>(&'a self, attachments: &'a [Attachment<'a, Metal>]) -> RenderPass
|
||||
where
|
||||
Metal: 'a,
|
||||
{
|
||||
Frame::render_pass(self, attachments)
|
||||
}
|
||||
|
||||
fn complete(self, sync: bool, completion: FrameCompletion) {
|
||||
Frame::complete(self, sync, completion);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn error_status_is_unhealthy() {
|
||||
assert_eq!(
|
||||
health_for_status(MTLCommandBufferStatus::Error),
|
||||
Health::Unhealthy
|
||||
);
|
||||
assert_eq!(
|
||||
health_for_status(MTLCommandBufferStatus::Completed),
|
||||
Health::Healthy
|
||||
);
|
||||
assert_eq!(
|
||||
health_for_status(MTLCommandBufferStatus::NotEnqueued),
|
||||
Health::Healthy
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
//! A wrapper around a CALayer with a utility method for settings its
|
||||
//! `contents` to an IOSurface (`metal/IOSurfaceLayer.zig`).
|
||||
use std::sync::{Arc, Mutex, PoisonError};
|
||||
|
||||
use dispatch2::DispatchQueue;
|
||||
use objc2::{
|
||||
AnyThread,
|
||||
DefinedClass,
|
||||
MainThreadMarker,
|
||||
define_class,
|
||||
msg_send,
|
||||
rc::Retained,
|
||||
runtime::AnyObject,
|
||||
};
|
||||
use objc2_core_foundation::CFRetained;
|
||||
use objc2_foundation::{NSNull, NSString};
|
||||
use objc2_io_surface::IOSurfaceRef;
|
||||
use objc2_quartz_core::{CALayer, kCAGravityTopLeft};
|
||||
|
||||
use super::Error;
|
||||
|
||||
/// The callback invoked when the layer needs to `display`.
|
||||
pub type DisplayCallback = Arc<dyn Fn() + Send + Sync>;
|
||||
|
||||
/// Instance variables of the layer subclass (`display_cb` / `display_ctx`).
|
||||
#[derive(Default)]
|
||||
pub struct Ivars {
|
||||
display_cb: Mutex<Option<DisplayCallback>>,
|
||||
}
|
||||
|
||||
define_class!(
|
||||
/// We subclass CALayer with a custom display handler.
|
||||
// SAFETY:
|
||||
// - The superclass CALayer has no subclassing requirements we violate.
|
||||
// - The class does not implement `Drop`.
|
||||
#[unsafe(super(CALayer))]
|
||||
#[name = "IOSurfaceLayer"]
|
||||
#[ivars = Ivars]
|
||||
pub struct IOSurfaceLayerClass;
|
||||
|
||||
impl IOSurfaceLayerClass {
|
||||
#[unsafe(method(display))]
|
||||
fn display(&self) {
|
||||
// Clone the callback out so it isn't invoked with the lock held.
|
||||
let cb = self
|
||||
.ivars()
|
||||
.display_cb
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.clone();
|
||||
if let Some(cb) = cb {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
|
||||
/// Disable all animations for this layer by returning null for all
|
||||
/// actions.
|
||||
#[unsafe(method_id(actionForKey:))]
|
||||
fn action_for_key(&self, _key: &NSString) -> Option<Retained<NSNull>> {
|
||||
Some(NSNull::null())
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl IOSurfaceLayerClass {
|
||||
fn create() -> Retained<Self> {
|
||||
let this = Self::alloc().set_ivars(Ivars::default());
|
||||
// SAFETY: `init` is the designated initializer of the superclass.
|
||||
unsafe { msg_send![super(this), init] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to move an Objective-C / CoreFoundation object to another thread.
|
||||
struct SendWrapper<T>(T);
|
||||
// SAFETY: used only for the layer and IOSurface, which are reference counted
|
||||
// thread-safe objects when messaged on the main thread (we only message the
|
||||
// layer on the main queue).
|
||||
unsafe impl<T> Send for SendWrapper<T> {}
|
||||
|
||||
/// A CALayer which displays IOSurfaces.
|
||||
#[derive(Clone)]
|
||||
pub struct IOSurfaceLayer {
|
||||
/// The underlying CALayer
|
||||
layer: Retained<IOSurfaceLayerClass>,
|
||||
}
|
||||
|
||||
// SAFETY: the layer is only messaged from the render / main threads through
|
||||
// thread-safe CALayer properties (`contents`, `bounds`, `contentsScale`), the
|
||||
// same way Ghostty (Zig) shares it between the renderer and the main thread.
|
||||
unsafe impl Send for IOSurfaceLayer {}
|
||||
// SAFETY: see above.
|
||||
unsafe impl Sync for IOSurfaceLayer {}
|
||||
|
||||
impl IOSurfaceLayer {
|
||||
pub fn new() -> Result<IOSurfaceLayer, Error> {
|
||||
let layer = IOSurfaceLayerClass::create();
|
||||
|
||||
// The layer gravity is set to top-left so that the contents aren't
|
||||
// stretched during resize operations before a new frame has been drawn.
|
||||
//
|
||||
// SAFETY: a constant NSString exported by QuartzCore.
|
||||
layer.setContentsGravity(unsafe { kCAGravityTopLeft });
|
||||
|
||||
Ok(IOSurfaceLayer { layer })
|
||||
}
|
||||
|
||||
/// The underlying CALayer.
|
||||
pub fn layer(&self) -> &CALayer {
|
||||
&self.layer
|
||||
}
|
||||
|
||||
/// The layer's bounds size and contents scale.
|
||||
pub fn bounds_and_scale(&self) -> (f64, f64, f64) {
|
||||
let bounds = self.layer.bounds();
|
||||
(
|
||||
bounds.size.width,
|
||||
bounds.size.height,
|
||||
self.layer.contentsScale(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the layer's `contents` to the provided IOSurface.
|
||||
///
|
||||
/// Makes sure to do so on the main thread to avoid visual artifacts.
|
||||
pub fn set_surface(&self, surface: &CFRetained<IOSurfaceRef>) {
|
||||
// We retain the surface (and the layer) to make sure they're not
|
||||
// released before we can set it as the contents of the layer; both
|
||||
// are released when the closure is dropped.
|
||||
let layer = SendWrapper(self.layer.clone());
|
||||
let surface = SendWrapper(surface.clone());
|
||||
|
||||
// We check if we're on the main thread and run the work directly if so.
|
||||
if MainThreadMarker::new().is_some() {
|
||||
set_surface_callback(&layer.0, &surface.0);
|
||||
} else {
|
||||
DispatchQueue::main().exec_async(move || {
|
||||
let layer = layer;
|
||||
let surface = surface;
|
||||
set_surface_callback(&layer.0, &surface.0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the layer's `contents` to the provided IOSurface.
|
||||
///
|
||||
/// Does not ensure this happens on the main thread.
|
||||
pub fn set_surface_sync(&self, surface: &CFRetained<IOSurfaceRef>) {
|
||||
let contents: &AnyObject = surface_as_object(surface);
|
||||
// SAFETY: an IOSurface is a valid value for `contents`.
|
||||
unsafe { self.layer.setContents(Some(contents)) };
|
||||
}
|
||||
|
||||
/// Set (or clear) the callback invoked from the layer's `display` method.
|
||||
pub fn set_display_callback(&self, cb: Option<DisplayCallback>) {
|
||||
*self
|
||||
.layer
|
||||
.ivars()
|
||||
.display_cb
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner) = cb;
|
||||
}
|
||||
}
|
||||
|
||||
/// View an IOSurface as an Objective-C object (they are toll-free bridged).
|
||||
fn surface_as_object(surface: &CFRetained<IOSurfaceRef>) -> &AnyObject {
|
||||
let cf: &objc2_core_foundation::CFType = surface;
|
||||
cf.as_ref()
|
||||
}
|
||||
|
||||
fn set_surface_callback(layer: &IOSurfaceLayerClass, surface: &CFRetained<IOSurfaceRef>) {
|
||||
// We check to see if the surface is the appropriate size for
|
||||
// the layer, if it's not then we discard it. This is because
|
||||
// asynchronously drawn frames can sometimes finish just after
|
||||
// a synchronously drawn frame during a resize, and if we don't
|
||||
// discard the improperly sized surface it creates jank.
|
||||
let bounds = layer.bounds();
|
||||
let scale = layer.contentsScale();
|
||||
let width = (bounds.size.width * scale) as usize;
|
||||
let height = (bounds.size.height * scale) as usize;
|
||||
if width != surface.width() || height != surface.height() {
|
||||
log::debug!(
|
||||
"setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {}x{}, layer = {}x{}",
|
||||
surface.width(),
|
||||
surface.height(),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// SAFETY: an IOSurface is a valid value for `contents`.
|
||||
unsafe { layer.setContents(Some(surface_as_object(surface))) };
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
//! Graphics API wrapper for Metal (`renderer/Metal.zig` and `renderer/metal/*`).
|
||||
//!
|
||||
//! The renderer draws into IOSurface-backed textures ([`target::Target`]) and
|
||||
//! presents them by setting the `contents` of an [`IOSurfaceLayer`], which is
|
||||
//! installed on the apprt's NSView (macOS: made "layer-hosting") or UIView
|
||||
//! (iOS: added as a sublayer).
|
||||
//!
|
||||
//! The platform-independent parts (enum values, GPU data layouts, pixel
|
||||
//! format selection, vertex attribute tables) live in
|
||||
//! [`crate::metal_layout`] so they can be unit tested on any host.
|
||||
//!
|
||||
//! Safety: this module talks to Metal / QuartzCore through `objc2`, which is
|
||||
//! mostly safe; `unsafe` is limited to encoder calls that take raw resources,
|
||||
//! raw pointer copies and the autorelease pool, each with a SAFETY comment.
|
||||
|
||||
use std::{ffi::c_void, sync::Arc};
|
||||
|
||||
use bytemuck::Pod;
|
||||
use ghostty_config::types_b::AlphaBlending;
|
||||
use ghostty_font::atlas::Atlas;
|
||||
use objc2::{
|
||||
msg_send,
|
||||
rc::Retained,
|
||||
runtime::{AnyObject, ProtocolObject},
|
||||
sel,
|
||||
};
|
||||
use objc2_foundation::NSError;
|
||||
use objc2_metal::{
|
||||
MTLBuffer,
|
||||
MTLCommandQueue,
|
||||
MTLDevice,
|
||||
MTLGPUFamily,
|
||||
MTLSamplerAddressMode,
|
||||
MTLSamplerMinMagFilter,
|
||||
};
|
||||
|
||||
pub mod buffer;
|
||||
pub mod frame;
|
||||
pub mod io_surface_layer;
|
||||
pub mod pipeline;
|
||||
pub mod render_pass;
|
||||
pub mod sampler;
|
||||
pub mod shaders;
|
||||
pub mod target;
|
||||
pub mod texture;
|
||||
|
||||
pub use self::{
|
||||
buffer::Buffer,
|
||||
frame::Frame,
|
||||
io_surface_layer::IOSurfaceLayer,
|
||||
pipeline::Pipeline,
|
||||
render_pass::RenderPass,
|
||||
sampler::Sampler,
|
||||
shaders::Shaders,
|
||||
target::Target,
|
||||
texture::Texture,
|
||||
};
|
||||
use crate::{
|
||||
graphics_api::{ApiError, GraphicsApi, ImageTextureFormat, TextureKind},
|
||||
metal_layout::{
|
||||
MetalUniforms,
|
||||
PixelFormat,
|
||||
ResourceOptions,
|
||||
StorageMode,
|
||||
TextureUsage,
|
||||
atlas_pixel_format,
|
||||
color_pixel_format,
|
||||
default_storage_mode,
|
||||
image_pixel_format,
|
||||
max_texture_size,
|
||||
surface_size,
|
||||
},
|
||||
shader_types::Uniforms,
|
||||
shadertoy,
|
||||
};
|
||||
|
||||
/// A `MTLDevice`.
|
||||
pub type Device = Retained<ProtocolObject<dyn MTLDevice>>;
|
||||
/// A `MTLCommandQueue`.
|
||||
pub type Queue = Retained<ProtocolObject<dyn MTLCommandQueue>>;
|
||||
|
||||
/// Custom shaders are converted to MSL for Metal.
|
||||
pub const CUSTOM_SHADER_TARGET: shadertoy::Target = shadertoy::Target::Msl;
|
||||
/// The fragCoord for Metal shaders is +Y = down.
|
||||
pub const CUSTOM_SHADER_Y_IS_DOWN: bool = true;
|
||||
/// Triple buffering.
|
||||
pub const SWAP_CHAIN_COUNT: usize = 3;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// A Metal API call failed.
|
||||
#[error("Metal API call failed")]
|
||||
MetalFailed,
|
||||
/// No usable Metal device exists.
|
||||
#[error("no Metal device")]
|
||||
NoMetalDevice,
|
||||
/// An Objective-C runtime operation failed.
|
||||
#[error("Objective-C runtime failure")]
|
||||
ObjCFailed,
|
||||
/// A shader function was not found in its library.
|
||||
#[error("shader function `{0}` not found")]
|
||||
MissingFunction(String),
|
||||
/// Metal reported an error (shader compilation, pipeline creation, ...).
|
||||
#[error("metal error: {0}")]
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Log and convert an `NSError` (`checkError`).
|
||||
pub(crate) fn from_ns_error(err: &NSError) -> Error {
|
||||
let desc = err.localizedDescription().to_string();
|
||||
log::error!("metal error={desc}");
|
||||
Error::Message(desc)
|
||||
}
|
||||
}
|
||||
|
||||
/// The NSView (macOS) / UIView (iOS) the renderer draws in, plus its content
|
||||
/// scale (Zig: `rt_surface.platform` and `rt_surface.content_scale.x`).
|
||||
pub struct ViewInfo<'a> {
|
||||
/// The NSView / UIView.
|
||||
pub view: &'a AnyObject,
|
||||
/// The scale factor of the window (backing scale factor).
|
||||
pub scale_factor: f64,
|
||||
}
|
||||
|
||||
/// An autorelease pool token (`objc_autoreleasePoolPush`).
|
||||
struct PoolToken(*mut c_void);
|
||||
|
||||
// SAFETY: the token is only used from the thread that pushed it (draw_frame
|
||||
// starts and ends the pool on one thread); `Send` is needed because the
|
||||
// graphics API lives inside the renderer's mutex.
|
||||
unsafe impl Send for PoolToken {}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn objc_autoreleasePoolPush() -> *mut c_void;
|
||||
fn objc_autoreleasePoolPop(pool: *mut c_void);
|
||||
}
|
||||
|
||||
/// The Metal graphics API.
|
||||
pub struct Metal {
|
||||
layer: IOSurfaceLayer,
|
||||
|
||||
/// MTLDevice
|
||||
device: Device,
|
||||
/// MTLCommandQueue
|
||||
queue: Queue,
|
||||
|
||||
/// Alpha blending mode
|
||||
blending: AlphaBlending,
|
||||
|
||||
/// The default storage mode to use for resources created with our device.
|
||||
///
|
||||
/// This is based on whether the device is a discrete GPU or not, since
|
||||
/// discrete GPUs do not have unified memory and therefore do not support
|
||||
/// the "shared" storage mode, instead we have to use the "managed" mode.
|
||||
default_storage_mode: StorageMode,
|
||||
|
||||
/// The maximum 2D texture width and height supported by the device.
|
||||
max_texture_size: u32,
|
||||
|
||||
/// We start an AutoreleasePool before `drawFrame` and end it afterwards.
|
||||
autorelease_pool: Option<PoolToken>,
|
||||
}
|
||||
|
||||
impl Metal {
|
||||
/// Create the Metal backend and attach its layer to `view`.
|
||||
///
|
||||
/// Must be called on the thread that owns the view (the main thread).
|
||||
pub fn init(blending: AlphaBlending, info: &ViewInfo<'_>) -> Result<Metal, Error> {
|
||||
// Choose our MTLDevice and create a MTLCommandQueue for that device.
|
||||
let device = choose_device()?;
|
||||
let queue = device.newCommandQueue().ok_or(Error::MetalFailed)?;
|
||||
|
||||
// Grab metadata about the device.
|
||||
let has_unified_memory = device.hasUnifiedMemory();
|
||||
let default_storage_mode =
|
||||
default_storage_mode(cfg!(target_os = "ios"), has_unified_memory);
|
||||
let max_texture_size = query_max_texture_size(&device);
|
||||
log::debug!(
|
||||
"device properties default_storage_mode={default_storage_mode:?} max_texture_size={max_texture_size}"
|
||||
);
|
||||
|
||||
// Create an IOSurfaceLayer which we can assign to the view to make
|
||||
// it in to a "layer-hosting view", so that we can manually control
|
||||
// the layer contents.
|
||||
let layer = IOSurfaceLayer::new()?;
|
||||
let layer_object: &AnyObject = layer.layer().as_ref();
|
||||
let view = info.view;
|
||||
|
||||
// Add our layer to the view.
|
||||
//
|
||||
// On macOS we do this by making the view "layer-hosting"
|
||||
// by assigning it to the view's `layer` property BEFORE
|
||||
// setting `wantsLayer` to `true`.
|
||||
//
|
||||
// On iOS, views are always layer-backed, and `layer`
|
||||
// is readonly, so instead we add it as a sublayer.
|
||||
//
|
||||
// SAFETY: `view` is an NSView / UIView (a documented requirement of
|
||||
// `ViewInfo`), which implements these selectors with these types.
|
||||
unsafe {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _: () = msg_send![view, setLayer: layer_object];
|
||||
let _: () = msg_send![view, setWantsLayer: true];
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let view_layer: Option<Retained<AnyObject>> = msg_send![view, layer];
|
||||
let view_layer = view_layer.ok_or(Error::ObjCFailed)?;
|
||||
let _: () = msg_send![&*view_layer, addSublayer: layer_object];
|
||||
}
|
||||
|
||||
// Ensure that if our layer is oversized it does not overflow the
|
||||
// bounds of the view. (`clipsToBounds` is NSView API since macOS
|
||||
// 14; older systems don't have the setter.)
|
||||
let responds: bool = msg_send![view, respondsToSelector: sel!(setClipsToBounds:)];
|
||||
if responds {
|
||||
let _: () = msg_send![view, setClipsToBounds: true];
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that our layer has a content scale set to
|
||||
// match the scale factor of the window. This avoids
|
||||
// magnification issues leading to blurry rendering.
|
||||
layer.layer().setContentsScale(info.scale_factor);
|
||||
|
||||
// This makes it so that our display callback will actually be called.
|
||||
layer.layer().setNeedsDisplayOnBoundsChange(true);
|
||||
|
||||
Ok(Metal {
|
||||
layer,
|
||||
device,
|
||||
queue,
|
||||
blending,
|
||||
default_storage_mode,
|
||||
max_texture_size,
|
||||
autorelease_pool: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The layer the renderer presents on.
|
||||
pub fn layer(&self) -> &IOSurfaceLayer {
|
||||
&self.layer
|
||||
}
|
||||
|
||||
/// The device the renderer draws with.
|
||||
pub fn device(&self) -> &Device {
|
||||
&self.device
|
||||
}
|
||||
|
||||
/// The default storage mode of resources created with this device.
|
||||
pub fn default_storage_mode(&self) -> StorageMode {
|
||||
self.default_storage_mode
|
||||
}
|
||||
|
||||
/// The maximum 2D texture size of the device.
|
||||
pub fn max_texture_size(&self) -> u32 {
|
||||
self.max_texture_size
|
||||
}
|
||||
|
||||
/// The pixel format of targets, custom shader textures and pipelines.
|
||||
fn color_format(&self) -> PixelFormat {
|
||||
color_pixel_format(self.blending)
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing buffers.
|
||||
pub fn buffer_options(&self) -> buffer::Options {
|
||||
buffer::Options {
|
||||
device: self.device.clone(),
|
||||
// Indicate that the CPU writes to this resource but never reads it.
|
||||
resource_options: ResourceOptions::write_combined(self.default_storage_mode),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing textures.
|
||||
pub fn texture_options(&self) -> texture::Options {
|
||||
texture::Options {
|
||||
device: self.device.clone(),
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode the
|
||||
// pixels written to it *after* blending, which means we get linear
|
||||
// alpha blending rather than gamma-incorrect blending.
|
||||
pixel_format: self.color_format(),
|
||||
resource_options: ResourceOptions::write_combined(self.default_storage_mode),
|
||||
// textureOptions is currently only used for custom shaders.
|
||||
usage: TextureUsage::custom_shader(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing samplers.
|
||||
pub fn sampler_options(&self) -> sampler::Options {
|
||||
sampler::Options {
|
||||
device: self.device.clone(),
|
||||
|
||||
// These parameters match Shadertoy behaviors.
|
||||
min_filter: MTLSamplerMinMagFilter::Linear,
|
||||
mag_filter: MTLSamplerMinMagFilter::Linear,
|
||||
s_address_mode: MTLSamplerAddressMode::ClampToEdge,
|
||||
t_address_mode: MTLSamplerAddressMode::ClampToEdge,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing textures for images.
|
||||
pub fn image_texture_options(
|
||||
&self,
|
||||
format: ImageTextureFormat,
|
||||
srgb: bool,
|
||||
) -> texture::Options {
|
||||
texture::Options {
|
||||
device: self.device.clone(),
|
||||
pixel_format: image_pixel_format(format, srgb),
|
||||
resource_options: ResourceOptions::write_combined(self.default_storage_mode),
|
||||
// We only need to read from this texture from a shader.
|
||||
usage: TextureUsage::shader_read_only(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a new render target which can be presented by this API.
|
||||
pub fn init_target(&self, width: usize, height: usize) -> Result<Target, Error> {
|
||||
Target::init(&target::Options {
|
||||
device: self.device.clone(),
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode the
|
||||
// pixels written to it *after* blending, which means we get linear
|
||||
// alpha blending rather than gamma-incorrect blending.
|
||||
pixel_format: self.color_format(),
|
||||
storage_mode: self.default_storage_mode,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Initializes a Texture suitable for the provided font atlas.
|
||||
pub fn init_atlas_texture(&self, atlas: &Atlas) -> Result<Texture, Error> {
|
||||
let pixel_format =
|
||||
atlas_pixel_format(atlas.format).expect("unsupported atlas format for Metal texture");
|
||||
|
||||
Texture::init(
|
||||
&texture::Options {
|
||||
device: self.device.clone(),
|
||||
pixel_format,
|
||||
resource_options: ResourceOptions::write_combined(self.default_storage_mode),
|
||||
// We only need to read from this texture from a shader.
|
||||
usage: TextureUsage::shader_read_only(),
|
||||
},
|
||||
atlas.size as usize,
|
||||
atlas.size as usize,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the current size of the runtime surface.
|
||||
pub fn surface_size(&self) -> (u32, u32) {
|
||||
let (width, height, scale) = self.layer.bounds_and_scale();
|
||||
|
||||
// We need to clamp our runtime surface size to the maximum
|
||||
// possible texture size since we can't create a screen buffer
|
||||
// (texture) larger than that.
|
||||
surface_size(width, height, scale, self.max_texture_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Metal {
|
||||
fn drop(&mut self) {
|
||||
// Nothing may call back into a renderer that is going away.
|
||||
self.layer.set_display_callback(None);
|
||||
if let Some(pool) = self.autorelease_pool.take() {
|
||||
// SAFETY: the pool was pushed by `draw_frame_start` on this thread.
|
||||
unsafe { objc_autoreleasePoolPop(pool.0) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines the maximum 2D texture size supported by the device.
|
||||
/// We need to clamp our frame size to this if it's larger.
|
||||
fn query_max_texture_size(device: &ProtocolObject<dyn MTLDevice>) -> u32 {
|
||||
// https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
|
||||
max_texture_size(
|
||||
device.supportsFamily(MTLGPUFamily::Apple10),
|
||||
device.supportsFamily(MTLGPUFamily::Apple3),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn choose_device() -> Result<Device, Error> {
|
||||
let devices = objc2_metal::MTLCopyAllDevices();
|
||||
|
||||
let mut chosen_device: Option<Device> = None;
|
||||
for device in &devices {
|
||||
// We want a GPU that’s connected to a display.
|
||||
if device.isHeadless() {
|
||||
continue;
|
||||
}
|
||||
chosen_device = Some(device.clone());
|
||||
// If the user has an eGPU plugged in, they probably want
|
||||
// to use it. Otherwise, integrated GPUs are better for
|
||||
// battery life and thermals.
|
||||
if device.isRemovable() || device.isLowPower() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
chosen_device.ok_or(Error::NoMetalDevice)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn choose_device() -> Result<Device, Error> {
|
||||
objc2_metal::MTLCreateSystemDefaultDevice().ok_or(Error::NoMetalDevice)
|
||||
}
|
||||
|
||||
impl GraphicsApi for Metal {
|
||||
const CUSTOM_SHADER_TARGET: shadertoy::Target = CUSTOM_SHADER_TARGET;
|
||||
const CUSTOM_SHADER_Y_IS_DOWN: bool = CUSTOM_SHADER_Y_IS_DOWN;
|
||||
const SWAP_CHAIN_COUNT: usize = SWAP_CHAIN_COUNT;
|
||||
|
||||
type Target = Target;
|
||||
type Texture = Texture;
|
||||
type Sampler = Sampler;
|
||||
type Buffer<T: Pod> = Buffer<T>;
|
||||
type BufferRef = Retained<ProtocolObject<dyn MTLBuffer>>;
|
||||
type Pipeline = Pipeline;
|
||||
type Shaders = Shaders;
|
||||
type GpuUniforms = MetalUniforms;
|
||||
type Frame = Frame;
|
||||
type RenderPass<'a> = RenderPass;
|
||||
|
||||
fn gpu_uniforms(uniforms: &Uniforms) -> MetalUniforms {
|
||||
MetalUniforms::from(uniforms)
|
||||
}
|
||||
|
||||
fn blending(&self) -> AlphaBlending {
|
||||
self.blending
|
||||
}
|
||||
|
||||
fn set_blending(&mut self, blending: AlphaBlending) {
|
||||
self.blending = blending;
|
||||
}
|
||||
|
||||
fn init_shaders(&self, custom_shaders: &[String]) -> Result<Shaders, ApiError> {
|
||||
Shaders::init(
|
||||
&self.device,
|
||||
custom_shaders,
|
||||
// Using an `*_srgb` pixel format makes Metal gamma encode the
|
||||
// pixels written to it *after* blending, which means we get linear
|
||||
// alpha blending rather than gamma-incorrect blending.
|
||||
self.color_format(),
|
||||
)
|
||||
.map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn surface_size(&self) -> Result<(u32, u32), ApiError> {
|
||||
Ok(Metal::surface_size(self))
|
||||
}
|
||||
|
||||
fn init_target(&self, width: usize, height: usize) -> Result<Target, ApiError> {
|
||||
Metal::init_target(self, width, height).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_buffer<T: Pod>(&self, len: usize) -> Result<Buffer<T>, ApiError> {
|
||||
Buffer::init(self.buffer_options(), len).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_buffer_fill<T: Pod>(&self, data: &[T]) -> Result<Buffer<T>, ApiError> {
|
||||
Buffer::init_fill(self.buffer_options(), data).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_texture(
|
||||
&self,
|
||||
kind: TextureKind,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Option<&[u8]>,
|
||||
) -> Result<Texture, ApiError> {
|
||||
let opts = match kind {
|
||||
TextureKind::CustomShader => self.texture_options(),
|
||||
TextureKind::Image { format, srgb } => self.image_texture_options(format, srgb),
|
||||
};
|
||||
Texture::init(&opts, width, height, data).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_atlas_texture(&self, atlas: &Atlas) -> Result<Texture, ApiError> {
|
||||
Metal::init_atlas_texture(self, atlas).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_sampler(&self) -> Result<Sampler, ApiError> {
|
||||
Sampler::init(&self.sampler_options()).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn begin_frame(&self, target: Arc<Target>) -> Frame {
|
||||
Frame::begin(&self.queue, self.layer.clone(), target)
|
||||
}
|
||||
|
||||
/// Present the last presented target again. (noop for Metal)
|
||||
fn present_last_target(&self) -> Result<(), ApiError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_last_target(&self) {}
|
||||
|
||||
/// Actions taken before doing anything in `drawFrame`.
|
||||
///
|
||||
/// Right now we use this to start an AutoreleasePool.
|
||||
fn draw_frame_start(&mut self) {
|
||||
debug_assert!(self.autorelease_pool.is_none());
|
||||
// SAFETY: balanced with the pop in `draw_frame_end` / `Drop`.
|
||||
self.autorelease_pool = Some(PoolToken(unsafe { objc_autoreleasePoolPush() }));
|
||||
}
|
||||
|
||||
/// Actions taken after `drawFrame` is done.
|
||||
///
|
||||
/// Right now we use this to end our AutoreleasePool.
|
||||
fn draw_frame_end(&mut self) {
|
||||
debug_assert!(self.autorelease_pool.is_some());
|
||||
if let Some(pool) = self.autorelease_pool.take() {
|
||||
// SAFETY: the pool was pushed by `draw_frame_start` on this thread.
|
||||
unsafe { objc_autoreleasePoolPop(pool.0) };
|
||||
}
|
||||
}
|
||||
|
||||
fn loop_enter(&mut self, draw: Arc<dyn Fn() + Send + Sync>) {
|
||||
self.layer.set_display_callback(Some(draw));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Wrapper for handling render pipelines (`metal/Pipeline.zig`).
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_foundation::NSString;
|
||||
use objc2_metal::{
|
||||
MTLBlendFactor,
|
||||
MTLBlendOperation,
|
||||
MTLDevice as _,
|
||||
MTLLibrary,
|
||||
MTLPixelFormat,
|
||||
MTLRenderPipelineDescriptor,
|
||||
MTLRenderPipelineState,
|
||||
MTLVertexDescriptor,
|
||||
MTLVertexFormat,
|
||||
MTLVertexStepFunction,
|
||||
};
|
||||
|
||||
use super::{Device, Error};
|
||||
use crate::metal_layout::{
|
||||
MetalVertex,
|
||||
PREMULTIPLIED_ALPHA,
|
||||
PixelFormat,
|
||||
VertexAttribute,
|
||||
VertexStepFunction,
|
||||
};
|
||||
|
||||
/// Describes a color attachment.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Attachment {
|
||||
pub pixel_format: PixelFormat,
|
||||
pub blending_enabled: bool,
|
||||
}
|
||||
|
||||
/// Options for initializing a render pipeline.
|
||||
pub struct Options<'a> {
|
||||
/// MTLDevice
|
||||
pub device: &'a Device,
|
||||
|
||||
/// Name of the vertex function
|
||||
pub vertex_fn: &'a str,
|
||||
/// Name of the fragment function
|
||||
pub fragment_fn: &'a str,
|
||||
|
||||
/// MTLLibrary to get the vertex function from
|
||||
pub vertex_library: &'a ProtocolObject<dyn MTLLibrary>,
|
||||
/// MTLLibrary to get the fragment function from
|
||||
pub fragment_library: &'a ProtocolObject<dyn MTLLibrary>,
|
||||
|
||||
/// Vertex step function
|
||||
pub step_fn: VertexStepFunction,
|
||||
|
||||
/// Info about the color attachments used by this render pipeline.
|
||||
pub attachments: &'a [Attachment],
|
||||
}
|
||||
|
||||
/// A render pipeline state.
|
||||
pub struct Pipeline {
|
||||
/// MTLRenderPipelineState
|
||||
state: Retained<ProtocolObject<dyn MTLRenderPipelineState>>,
|
||||
}
|
||||
|
||||
// SAFETY: MTLRenderPipelineState objects are immutable and thread-safe.
|
||||
unsafe impl Send for Pipeline {}
|
||||
// SAFETY: see above.
|
||||
unsafe impl Sync for Pipeline {}
|
||||
|
||||
impl Pipeline {
|
||||
/// The underlying MTLRenderPipelineState.
|
||||
pub fn state(&self) -> &ProtocolObject<dyn MTLRenderPipelineState> {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// Create a pipeline whose vertex attributes are those of `V`.
|
||||
pub fn init<V: MetalVertex>(opts: &Options<'_>) -> Result<Pipeline, Error> {
|
||||
Self::init_impl(Some((V::ATTRIBUTES, size_of::<V>())), opts)
|
||||
}
|
||||
|
||||
/// Create a pipeline without a vertex descriptor.
|
||||
pub fn init_no_attributes(opts: &Options<'_>) -> Result<Pipeline, Error> {
|
||||
Self::init_impl(None, opts)
|
||||
}
|
||||
|
||||
fn init_impl(
|
||||
vertex_attributes: Option<(&[VertexAttribute], usize)>,
|
||||
opts: &Options<'_>,
|
||||
) -> Result<Pipeline, Error> {
|
||||
// Create our descriptor
|
||||
let desc = MTLRenderPipelineDescriptor::new();
|
||||
|
||||
// Get our vertex and fragment functions and add them to the descriptor.
|
||||
{
|
||||
let name = NSString::from_str(opts.vertex_fn);
|
||||
let func_vert = opts
|
||||
.vertex_library
|
||||
.newFunctionWithName(&name)
|
||||
.ok_or_else(|| Error::MissingFunction(opts.vertex_fn.to_owned()))?;
|
||||
desc.setVertexFunction(Some(&func_vert));
|
||||
}
|
||||
{
|
||||
let name = NSString::from_str(opts.fragment_fn);
|
||||
let func_frag = opts
|
||||
.fragment_library
|
||||
.newFunctionWithName(&name)
|
||||
.ok_or_else(|| Error::MissingFunction(opts.fragment_fn.to_owned()))?;
|
||||
desc.setFragmentFunction(Some(&func_frag));
|
||||
}
|
||||
|
||||
// If we have vertex attributes, create and add a vertex descriptor.
|
||||
if let Some((attributes, stride)) = vertex_attributes {
|
||||
let vertex_desc = MTLVertexDescriptor::new();
|
||||
|
||||
// Our attributes are the fields of the input
|
||||
let attrs = vertex_desc.attributes();
|
||||
for (i, a) in attributes.iter().enumerate() {
|
||||
// SAFETY: indexing the attribute array with a valid index
|
||||
// (the descriptor array has 31 slots); offsets and buffer
|
||||
// index are plain numbers validated by pipeline creation.
|
||||
unsafe {
|
||||
let attr = attrs.objectAtIndexedSubscript(i);
|
||||
attr.setFormat(MTLVertexFormat(a.format as usize));
|
||||
attr.setOffset(a.offset);
|
||||
attr.setBufferIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
// The layout describes how and when we fetch the next vertex input.
|
||||
let layouts = vertex_desc.layouts();
|
||||
// SAFETY: buffer layout 0 exists; the stride is a plain number.
|
||||
unsafe {
|
||||
let layout = layouts.objectAtIndexedSubscript(0);
|
||||
layout.setStepFunction(MTLVertexStepFunction(opts.step_fn as usize));
|
||||
layout.setStride(stride);
|
||||
}
|
||||
|
||||
desc.setVertexDescriptor(Some(&vertex_desc));
|
||||
}
|
||||
|
||||
// Set our color attachment
|
||||
let attachments = desc.colorAttachments();
|
||||
for (i, at) in opts.attachments.iter().enumerate() {
|
||||
// SAFETY: color attachment indexes are validated by Metal; the
|
||||
// pipelines use a single attachment.
|
||||
let attachment = unsafe { attachments.objectAtIndexedSubscript(i) };
|
||||
|
||||
attachment.setPixelFormat(MTLPixelFormat(at.pixel_format as usize));
|
||||
|
||||
attachment.setBlendingEnabled(at.blending_enabled);
|
||||
// We always use premultiplied alpha blending for now.
|
||||
if at.blending_enabled {
|
||||
let b = PREMULTIPLIED_ALPHA;
|
||||
attachment.setRgbBlendOperation(MTLBlendOperation(b.rgb_operation as usize));
|
||||
attachment.setAlphaBlendOperation(MTLBlendOperation(b.alpha_operation as usize));
|
||||
attachment.setSourceRGBBlendFactor(MTLBlendFactor(b.source_rgb as usize));
|
||||
attachment.setSourceAlphaBlendFactor(MTLBlendFactor(b.source_alpha as usize));
|
||||
attachment.setDestinationRGBBlendFactor(MTLBlendFactor(b.destination_rgb as usize));
|
||||
attachment
|
||||
.setDestinationAlphaBlendFactor(MTLBlendFactor(b.destination_alpha as usize));
|
||||
}
|
||||
}
|
||||
|
||||
// Make our state
|
||||
let state = opts
|
||||
.device
|
||||
.newRenderPipelineStateWithDescriptor_error(&desc)
|
||||
.map_err(|err| Error::from_ns_error(&err))?;
|
||||
|
||||
Ok(Pipeline { state })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Wrapper for handling render passes (`metal/RenderPass.zig`).
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_metal::{
|
||||
MTLClearColor,
|
||||
MTLCommandBuffer,
|
||||
MTLCommandEncoder as _,
|
||||
MTLLoadAction,
|
||||
MTLPrimitiveType,
|
||||
MTLRenderCommandEncoder,
|
||||
MTLRenderPassDescriptor,
|
||||
MTLStoreAction,
|
||||
};
|
||||
|
||||
use super::Metal;
|
||||
use crate::{
|
||||
graphics_api::{Attachment, AttachmentTarget, GpuRenderPass, Primitive, Step},
|
||||
metal_layout::{UNIFORMS_BUFFER_INDEX, buffer_index},
|
||||
};
|
||||
|
||||
/// The `MTLPrimitiveType` of a [`Primitive`].
|
||||
fn primitive_type(p: Primitive) -> MTLPrimitiveType {
|
||||
match p {
|
||||
Primitive::Point => MTLPrimitiveType::Point,
|
||||
Primitive::Line => MTLPrimitiveType::Line,
|
||||
Primitive::LineStrip => MTLPrimitiveType::LineStrip,
|
||||
Primitive::Triangle => MTLPrimitiveType::Triangle,
|
||||
Primitive::TriangleStrip => MTLPrimitiveType::TriangleStrip,
|
||||
}
|
||||
}
|
||||
|
||||
/// A render pass being encoded.
|
||||
pub struct RenderPass {
|
||||
/// MTLRenderCommandEncoder (`None` if the encoder couldn't be created, in
|
||||
/// which case every step is a no-op).
|
||||
encoder: Option<Retained<ProtocolObject<dyn MTLRenderCommandEncoder>>>,
|
||||
}
|
||||
|
||||
impl RenderPass {
|
||||
/// Begin a render pass.
|
||||
pub fn begin(
|
||||
command_buffer: Option<&ProtocolObject<dyn MTLCommandBuffer>>,
|
||||
attachments: &[Attachment<'_, Metal>],
|
||||
) -> RenderPass {
|
||||
let Some(command_buffer) = command_buffer else {
|
||||
return RenderPass { encoder: None };
|
||||
};
|
||||
// Create a pass descriptor
|
||||
let desc = MTLRenderPassDescriptor::renderPassDescriptor();
|
||||
|
||||
// Set our color attachment to be our drawable surface.
|
||||
let color_attachments = desc.colorAttachments();
|
||||
for (i, at) in attachments.iter().enumerate() {
|
||||
// SAFETY: color attachment indexes are validated by Metal.
|
||||
let attachment = unsafe { color_attachments.objectAtIndexedSubscript(i) };
|
||||
|
||||
attachment.setLoadAction(if at.clear_color.is_some() {
|
||||
MTLLoadAction::Clear
|
||||
} else {
|
||||
MTLLoadAction::Load
|
||||
});
|
||||
attachment.setStoreAction(MTLStoreAction::Store);
|
||||
attachment.setTexture(Some(match &at.target {
|
||||
AttachmentTarget::Texture(t) => t.texture(),
|
||||
AttachmentTarget::Target(t) => t.texture(),
|
||||
}));
|
||||
if let Some(c) = at.clear_color {
|
||||
attachment.setClearColor(MTLClearColor {
|
||||
red: f64::from(c[0]),
|
||||
green: f64::from(c[1]),
|
||||
blue: f64::from(c[2]),
|
||||
alpha: f64::from(c[3]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// MTLRenderCommandEncoder
|
||||
let encoder = command_buffer.renderCommandEncoderWithDescriptor(&desc);
|
||||
if encoder.is_none() {
|
||||
log::warn!("unable to create a Metal render command encoder");
|
||||
}
|
||||
|
||||
RenderPass { encoder }
|
||||
}
|
||||
|
||||
/// Add a step to this render pass.
|
||||
pub fn step(&self, s: &Step<'_, Metal>) {
|
||||
let Some(encoder) = &self.encoder else {
|
||||
return;
|
||||
};
|
||||
if s.draw.instance_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set pipeline state
|
||||
encoder.setRenderPipelineState(s.pipeline.state());
|
||||
|
||||
if !s.buffers.is_empty() {
|
||||
// We set buffers for both stages, at index 0 for the vertex
|
||||
// buffer (this isn't very flexible but it lines up with the API we
|
||||
// have for OpenGL) and the rest starting at index 2, so that we
|
||||
// can use index 1 for the uniforms if present.
|
||||
//
|
||||
// Also, we set buffers (and textures) for both stages.
|
||||
//
|
||||
// Again, not very flexible, but it's consistent and predictable,
|
||||
// and we need to treat the uniforms as special because of OpenGL.
|
||||
//
|
||||
// TODO: Maybe in the future add info to the pipeline struct which
|
||||
// allows it to define a mapping between provided buffers and
|
||||
// what index they get set at for the vertex / fragment stage.
|
||||
for (i, b) in s.buffers.iter().enumerate() {
|
||||
if let Some(buf) = b {
|
||||
// SAFETY: the buffer is a live MTLBuffer.
|
||||
unsafe {
|
||||
encoder.setVertexBuffer_offset_atIndex(Some(buf), 0, buffer_index(i));
|
||||
encoder.setFragmentBuffer_offset_atIndex(Some(buf), 0, buffer_index(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the uniforms as buffer index 1 if present.
|
||||
if let Some(buf) = &s.uniforms {
|
||||
// SAFETY: the buffer is a live MTLBuffer.
|
||||
unsafe {
|
||||
encoder.setVertexBuffer_offset_atIndex(Some(buf), 0, UNIFORMS_BUFFER_INDEX);
|
||||
encoder.setFragmentBuffer_offset_atIndex(Some(buf), 0, UNIFORMS_BUFFER_INDEX);
|
||||
}
|
||||
}
|
||||
|
||||
// Set textures.
|
||||
for (i, t) in s.textures.iter().enumerate() {
|
||||
if let Some(tex) = t {
|
||||
// SAFETY: the texture is a live MTLTexture.
|
||||
unsafe {
|
||||
encoder.setVertexTexture_atIndex(Some(tex.texture()), i);
|
||||
encoder.setFragmentTexture_atIndex(Some(tex.texture()), i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set samplers.
|
||||
for (i, samp) in s.samplers.iter().enumerate() {
|
||||
if let Some(sampler) = samp {
|
||||
// SAFETY: the sampler is a live MTLSamplerState.
|
||||
unsafe {
|
||||
encoder.setFragmentSamplerState_atIndex(Some(sampler.sampler()), i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw!
|
||||
//
|
||||
// SAFETY: all resources the pipeline reads were bound above (the
|
||||
// same contract as the Zig renderer; Metal validation reports misuse).
|
||||
unsafe {
|
||||
encoder.drawPrimitives_vertexStart_vertexCount_instanceCount(
|
||||
primitive_type(s.draw.ty),
|
||||
0,
|
||||
s.draw.vertex_count,
|
||||
s.draw.instance_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete this render pass.
|
||||
/// This struct can no longer be used after calling this.
|
||||
pub fn complete(self) {
|
||||
if let Some(encoder) = &self.encoder {
|
||||
encoder.endEncoding();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuRenderPass<'_, Metal> for RenderPass {
|
||||
fn step(&mut self, step: &Step<'_, Metal>) {
|
||||
RenderPass::step(self, step);
|
||||
}
|
||||
|
||||
fn complete(self) {
|
||||
RenderPass::complete(self);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn primitive_mapping() {
|
||||
// MTLPrimitiveType raw values (api.zig).
|
||||
assert_eq!(primitive_type(Primitive::Point).0, 0);
|
||||
assert_eq!(primitive_type(Primitive::Line).0, 1);
|
||||
assert_eq!(primitive_type(Primitive::LineStrip).0, 2);
|
||||
assert_eq!(primitive_type(Primitive::Triangle).0, 3);
|
||||
assert_eq!(primitive_type(Primitive::TriangleStrip).0, 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Wrapper for handling samplers (`metal/Sampler.zig`).
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_metal::{
|
||||
MTLDevice as _,
|
||||
MTLSamplerAddressMode,
|
||||
MTLSamplerDescriptor,
|
||||
MTLSamplerMinMagFilter,
|
||||
MTLSamplerState,
|
||||
};
|
||||
|
||||
use super::{Device, Error};
|
||||
|
||||
/// Options for initializing a sampler.
|
||||
#[derive(Clone)]
|
||||
pub struct Options {
|
||||
/// MTLDevice
|
||||
pub device: Device,
|
||||
pub min_filter: MTLSamplerMinMagFilter,
|
||||
pub mag_filter: MTLSamplerMinMagFilter,
|
||||
pub s_address_mode: MTLSamplerAddressMode,
|
||||
pub t_address_mode: MTLSamplerAddressMode,
|
||||
}
|
||||
|
||||
/// A sampler state.
|
||||
pub struct Sampler {
|
||||
/// The underlying MTLSamplerState Object.
|
||||
sampler: Retained<ProtocolObject<dyn MTLSamplerState>>,
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
/// The underlying MTLSamplerState.
|
||||
pub fn sampler(&self) -> &ProtocolObject<dyn MTLSamplerState> {
|
||||
&self.sampler
|
||||
}
|
||||
|
||||
/// Initialize a sampler
|
||||
pub fn init(opts: &Options) -> Result<Sampler, Error> {
|
||||
// Create our descriptor and set the properties
|
||||
let desc = MTLSamplerDescriptor::new();
|
||||
desc.setMinFilter(opts.min_filter);
|
||||
desc.setMagFilter(opts.mag_filter);
|
||||
desc.setSAddressMode(opts.s_address_mode);
|
||||
desc.setTAddressMode(opts.t_address_mode);
|
||||
|
||||
// Create the sampler state
|
||||
let sampler = opts
|
||||
.device
|
||||
.newSamplerStateWithDescriptor(&desc)
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
|
||||
Ok(Sampler { sampler })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Shader pipelines and GPU data layouts for Metal (`metal/shaders.zig`).
|
||||
//!
|
||||
//! The CPU-side data layouts are shared with OpenGL
|
||||
//! ([`crate::shader_types`]) except for the uniform block, see
|
||||
//! [`crate::metal_layout::MetalUniforms`].
|
||||
use std::time::Instant;
|
||||
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_foundation::NSString;
|
||||
use objc2_metal::{MTLDevice as _, MTLLibrary};
|
||||
|
||||
use super::{
|
||||
Device,
|
||||
Error,
|
||||
pipeline::{self, Attachment, Pipeline},
|
||||
};
|
||||
use crate::{
|
||||
graphics_api::GpuShaders,
|
||||
metal_layout::{
|
||||
FULL_SCREEN_VERTEX_FN,
|
||||
PIPELINE_DESCS,
|
||||
POST_FRAGMENT_FN,
|
||||
PipelineDesc,
|
||||
PixelFormat,
|
||||
VertexStepFunction,
|
||||
},
|
||||
shader_types::{BgImage, CellText, Image as ImageVertex},
|
||||
};
|
||||
|
||||
/// The Metal shading language source of all built-in shaders
|
||||
/// (`shaders/shaders.metal`). Ghostty compiles this to a `.metallib` at build
|
||||
/// time with `xcrun metal`; we compile it with `newLibraryWithSource` when the
|
||||
/// shaders are initialized.
|
||||
pub const SHADERS_METAL: &str = include_str!("../../shaders/shaders.metal");
|
||||
|
||||
/// This contains the state for the shaders used by the Metal renderer.
|
||||
pub struct Shaders {
|
||||
library: Retained<ProtocolObject<dyn MTLLibrary>>,
|
||||
|
||||
pub bg_color: Pipeline,
|
||||
pub cell_bg: Pipeline,
|
||||
pub cell_text: Pipeline,
|
||||
pub image: Pipeline,
|
||||
pub bg_image: Pipeline,
|
||||
|
||||
/// 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
|
||||
/// against the output of the previous shader.
|
||||
pub post_pipelines: Vec<Pipeline>,
|
||||
}
|
||||
|
||||
// SAFETY: MTLLibrary objects are thread-safe, and pipelines are immutable.
|
||||
unsafe impl Send for Shaders {}
|
||||
// SAFETY: see above.
|
||||
unsafe impl Sync for Shaders {}
|
||||
|
||||
impl Shaders {
|
||||
/// Initialize our shader set.
|
||||
///
|
||||
/// "post_shaders" is an optional list of postprocess shaders to run
|
||||
/// against the final drawable texture. This is an array of shader source
|
||||
/// code, not file paths.
|
||||
pub fn init<S: AsRef<str>>(
|
||||
device: &Device,
|
||||
post_shaders: &[S],
|
||||
pixel_format: PixelFormat,
|
||||
) -> Result<Shaders, Error> {
|
||||
let library = init_library(device)?;
|
||||
|
||||
let make = |desc: &PipelineDesc| -> Result<Pipeline, Error> {
|
||||
let opts = pipeline::Options {
|
||||
device,
|
||||
vertex_fn: desc.vertex_fn,
|
||||
fragment_fn: desc.fragment_fn,
|
||||
vertex_library: &library,
|
||||
fragment_library: &library,
|
||||
step_fn: desc.step_fn,
|
||||
attachments: &[Attachment {
|
||||
pixel_format,
|
||||
blending_enabled: desc.blending_enabled,
|
||||
}],
|
||||
};
|
||||
match desc.name {
|
||||
"cell_text" => Pipeline::init::<CellText>(&opts),
|
||||
"image" => Pipeline::init::<ImageVertex>(&opts),
|
||||
"bg_image" => Pipeline::init::<BgImage>(&opts),
|
||||
_ => Pipeline::init_no_attributes(&opts),
|
||||
}
|
||||
};
|
||||
let [bg_color, cell_bg, cell_text, image, bg_image] = &PIPELINE_DESCS;
|
||||
|
||||
let bg_color = make(bg_color)?;
|
||||
let cell_bg = make(cell_bg)?;
|
||||
let cell_text = make(cell_text)?;
|
||||
let image = make(image)?;
|
||||
let bg_image = make(bg_image)?;
|
||||
|
||||
let post_pipelines = init_post_pipelines(device, &library, post_shaders, pixel_format)
|
||||
.unwrap_or_else(|err| {
|
||||
// If an error happens while building postprocess shaders we
|
||||
// want to just not use any postprocess shaders since we don't
|
||||
// want to block Ghostty from working.
|
||||
log::warn!("error initializing postprocess shaders err={err}");
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
Ok(Shaders {
|
||||
library,
|
||||
bg_color,
|
||||
cell_bg,
|
||||
cell_text,
|
||||
image,
|
||||
bg_image,
|
||||
post_pipelines,
|
||||
})
|
||||
}
|
||||
|
||||
/// The compiled library of the built-in shaders.
|
||||
pub fn library(&self) -> &ProtocolObject<dyn MTLLibrary> {
|
||||
&self.library
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuShaders<super::Metal> for Shaders {
|
||||
fn bg_color(&self) -> &Pipeline {
|
||||
&self.bg_color
|
||||
}
|
||||
|
||||
fn cell_bg(&self) -> &Pipeline {
|
||||
&self.cell_bg
|
||||
}
|
||||
|
||||
fn cell_text(&self) -> &Pipeline {
|
||||
&self.cell_text
|
||||
}
|
||||
|
||||
fn image(&self) -> &Pipeline {
|
||||
&self.image
|
||||
}
|
||||
|
||||
fn bg_image(&self) -> &Pipeline {
|
||||
&self.bg_image
|
||||
}
|
||||
|
||||
fn post_pipelines(&self) -> &[Pipeline] {
|
||||
&self.post_pipelines
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
|
||||
fn init_library(device: &Device) -> Result<Retained<ProtocolObject<dyn MTLLibrary>>, Error> {
|
||||
let start = Instant::now();
|
||||
|
||||
let source = NSString::from_str(SHADERS_METAL);
|
||||
let library = device
|
||||
.newLibraryWithSource_options_error(&source, None)
|
||||
.map_err(|err| Error::from_ns_error(&err))?;
|
||||
|
||||
log::debug!(
|
||||
"shader library loaded time={}us",
|
||||
start.elapsed().as_micros()
|
||||
);
|
||||
|
||||
Ok(library)
|
||||
}
|
||||
|
||||
/// Initialize our custom shader pipelines.
|
||||
///
|
||||
/// The shaders argument is a set of shader source code, not file paths.
|
||||
fn init_post_pipelines<S: AsRef<str>>(
|
||||
device: &Device,
|
||||
library: &ProtocolObject<dyn MTLLibrary>,
|
||||
shaders: &[S],
|
||||
pixel_format: PixelFormat,
|
||||
) -> Result<Vec<Pipeline>, Error> {
|
||||
// If we have no shaders, do nothing. If any error happens the
|
||||
// already-built pipelines are dropped (released).
|
||||
shaders
|
||||
.iter()
|
||||
.map(|source| init_post_pipeline(device, library, source.as_ref(), pixel_format))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Initialize a single custom shader pipeline from shader source.
|
||||
fn init_post_pipeline(
|
||||
device: &Device,
|
||||
library: &ProtocolObject<dyn MTLLibrary>,
|
||||
data: &str,
|
||||
pixel_format: PixelFormat,
|
||||
) -> Result<Pipeline, Error> {
|
||||
// Create our library which has the shader source
|
||||
let source = NSString::from_str(data);
|
||||
let post_library = device
|
||||
.newLibraryWithSource_options_error(&source, None)
|
||||
.map_err(|err| Error::from_ns_error(&err))?;
|
||||
|
||||
Pipeline::init_no_attributes(&pipeline::Options {
|
||||
device,
|
||||
vertex_fn: FULL_SCREEN_VERTEX_FN,
|
||||
fragment_fn: POST_FRAGMENT_FN,
|
||||
vertex_library: library,
|
||||
fragment_library: &post_library,
|
||||
step_fn: VertexStepFunction::PerVertex,
|
||||
attachments: &[Attachment {
|
||||
pixel_format,
|
||||
blending_enabled: false,
|
||||
}],
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
//! Represents a render target (`metal/Target.zig`).
|
||||
//!
|
||||
//! In this case, an IOSurface-backed MTLTexture.
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_core_foundation::{CFDictionary, CFNumber, CFRetained, CFString, CFType};
|
||||
use objc2_core_graphics::{CGColorSpace, kCGColorSpaceDisplayP3};
|
||||
use objc2_io_surface::{
|
||||
IOSurfaceRef,
|
||||
kIOSurfaceBytesPerElement,
|
||||
kIOSurfaceColorSpace,
|
||||
kIOSurfaceHeight,
|
||||
kIOSurfacePixelFormat,
|
||||
kIOSurfaceWidth,
|
||||
};
|
||||
use objc2_metal::{
|
||||
MTLDevice as _,
|
||||
MTLPixelFormat,
|
||||
MTLResourceOptions,
|
||||
MTLTexture,
|
||||
MTLTextureDescriptor,
|
||||
MTLTextureUsage,
|
||||
};
|
||||
|
||||
use super::{Device, Error};
|
||||
use crate::{
|
||||
graphics_api::GpuTarget,
|
||||
metal_layout::{PixelFormat, ResourceOptions, StorageMode, TextureUsage},
|
||||
};
|
||||
|
||||
/// `kCVPixelFormatType_32BGRA` ('BGRA'): `video.PixelFormat.@"32BGRA"`.
|
||||
pub const PIXEL_FORMAT_32BGRA: i32 = 0x4247_5241;
|
||||
|
||||
/// `kIOSurfacePurgeableEmpty`.
|
||||
const IOSURFACE_PURGEABLE_EMPTY: u32 = 2;
|
||||
|
||||
/// Options for initializing a Target
|
||||
pub struct Options {
|
||||
/// MTLDevice
|
||||
pub device: Device,
|
||||
|
||||
/// Desired width
|
||||
pub width: usize,
|
||||
/// Desired height
|
||||
pub height: usize,
|
||||
|
||||
/// Pixel format for the MTLTexture
|
||||
pub pixel_format: PixelFormat,
|
||||
/// Storage mode for the MTLTexture
|
||||
pub storage_mode: StorageMode,
|
||||
}
|
||||
|
||||
/// An IOSurface-backed render target.
|
||||
pub struct Target {
|
||||
/// The underlying IOSurface.
|
||||
surface: CFRetained<IOSurfaceRef>,
|
||||
|
||||
/// The underlying MTLTexture.
|
||||
texture: Retained<ProtocolObject<dyn MTLTexture>>,
|
||||
|
||||
/// Current width of this target.
|
||||
pub width: usize,
|
||||
/// Current height of this target.
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
// SAFETY: IOSurfaces and MTLTextures are thread-safe reference counted
|
||||
// objects; the target is only mutated by the GPU.
|
||||
unsafe impl Send for Target {}
|
||||
// SAFETY: see above.
|
||||
unsafe impl Sync for Target {}
|
||||
|
||||
impl Target {
|
||||
/// The underlying IOSurface.
|
||||
pub fn surface(&self) -> &CFRetained<IOSurfaceRef> {
|
||||
&self.surface
|
||||
}
|
||||
|
||||
/// The underlying MTLTexture.
|
||||
pub fn texture(&self) -> &ProtocolObject<dyn MTLTexture> {
|
||||
&self.texture
|
||||
}
|
||||
|
||||
pub fn init(opts: &Options) -> Result<Target, Error> {
|
||||
// We set our surface's color space to Display P3.
|
||||
// This allows us to have "Apple-style" alpha blending,
|
||||
// since it seems to be the case that Apple apps like
|
||||
// Terminal and TextEdit render text in the display's
|
||||
// color space using converted colors, which reduces,
|
||||
// but does not fully eliminate blending artifacts.
|
||||
//
|
||||
// SAFETY: a constant CFString exported by CoreGraphics.
|
||||
let colorspace = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceDisplayP3 }))
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
|
||||
let surface = create_surface(opts.width, opts.height, &colorspace)?;
|
||||
|
||||
// Create our descriptor and set our properties
|
||||
let desc = MTLTextureDescriptor::new();
|
||||
// SAFETY: plain numbers, validated when the texture is created.
|
||||
unsafe {
|
||||
desc.setWidth(opts.width);
|
||||
desc.setHeight(opts.height);
|
||||
}
|
||||
desc.setPixelFormat(MTLPixelFormat(opts.pixel_format as usize));
|
||||
desc.setUsage(MTLTextureUsage(
|
||||
TextureUsage {
|
||||
render_target: true,
|
||||
..TextureUsage::default()
|
||||
}
|
||||
.bits() as usize,
|
||||
));
|
||||
desc.setResourceOptions(MTLResourceOptions(
|
||||
// Indicate that the CPU writes to this resource but never reads it.
|
||||
ResourceOptions::write_combined(opts.storage_mode).bits() as usize,
|
||||
));
|
||||
|
||||
let texture = opts
|
||||
.device
|
||||
.newTextureWithDescriptor_iosurface_plane(&desc, &surface, 0)
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
|
||||
Ok(Target {
|
||||
surface,
|
||||
texture,
|
||||
width: opts.width,
|
||||
height: opts.height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Target {
|
||||
fn drop(&mut self) {
|
||||
// We mark it purgeable so that it is immediately unloaded, so that we
|
||||
// don't have to wait for CoreFoundation garbage collection to trigger.
|
||||
//
|
||||
// SAFETY: a null `old_state` is allowed.
|
||||
unsafe {
|
||||
self.surface
|
||||
.set_purgeable(IOSURFACE_PURGEABLE_EMPTY, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuTarget for Target {
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
/// `IOSurface.init`: a BGRA IOSurface of the given size with a color space.
|
||||
fn create_surface(
|
||||
width: usize,
|
||||
height: usize,
|
||||
colorspace: &CGColorSpace,
|
||||
) -> Result<CFRetained<IOSurfaceRef>, Error> {
|
||||
let w = CFNumber::new_i32(i32::try_from(width).map_err(|_| Error::MetalFailed)?);
|
||||
let h = CFNumber::new_i32(i32::try_from(height).map_err(|_| Error::MetalFailed)?);
|
||||
let pf = CFNumber::new_i32(PIXEL_FORMAT_32BGRA);
|
||||
let bpe = CFNumber::new_i32(4);
|
||||
|
||||
// SAFETY: constant CFStrings exported by IOSurface.
|
||||
let keys: [&CFString; 4] = unsafe {
|
||||
[
|
||||
kIOSurfaceWidth,
|
||||
kIOSurfaceHeight,
|
||||
kIOSurfacePixelFormat,
|
||||
kIOSurfaceBytesPerElement,
|
||||
]
|
||||
};
|
||||
let values: [&CFNumber; 4] = [&w, &h, &pf, &bpe];
|
||||
let properties = CFDictionary::<CFString, CFNumber>::from_slices(&keys, &values);
|
||||
|
||||
// SAFETY: the dictionary has the CFString keys / CFNumber values
|
||||
// IOSurfaceCreate expects.
|
||||
let surface = unsafe { IOSurfaceRef::new(properties.as_opaque()) }.ok_or(Error::MetalFailed)?;
|
||||
|
||||
// setColorSpace: serialize the colorspace into the surface properties.
|
||||
if let Some(serialized) = colorspace.property_list() {
|
||||
let value: &CFType = serialized.as_ref();
|
||||
// SAFETY: the color space key takes a property list value.
|
||||
unsafe { surface.set_value(kIOSurfaceColorSpace, value) };
|
||||
}
|
||||
|
||||
Ok(surface)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn bgra_fourcc() {
|
||||
assert_eq!(PIXEL_FORMAT_32BGRA, i32::from_be_bytes(*b"BGRA"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Wrapper for handling textures (`metal/Texture.zig`).
|
||||
use std::{ffi::c_void, ptr::NonNull};
|
||||
|
||||
use objc2::{rc::Retained, runtime::ProtocolObject};
|
||||
use objc2_metal::{
|
||||
MTLDevice as _,
|
||||
MTLOrigin,
|
||||
MTLPixelFormat,
|
||||
MTLRegion,
|
||||
MTLResourceOptions,
|
||||
MTLSize,
|
||||
MTLTexture,
|
||||
MTLTextureDescriptor,
|
||||
MTLTextureUsage,
|
||||
};
|
||||
|
||||
use super::{Device, Error};
|
||||
use crate::{
|
||||
graphics_api::{ApiError, GpuTexture},
|
||||
metal_layout::{PixelFormat, ResourceOptions, TextureUsage},
|
||||
};
|
||||
|
||||
/// Options for initializing a texture.
|
||||
#[derive(Clone)]
|
||||
pub struct Options {
|
||||
/// MTLDevice
|
||||
pub device: Device,
|
||||
pub pixel_format: PixelFormat,
|
||||
pub resource_options: ResourceOptions,
|
||||
pub usage: TextureUsage,
|
||||
}
|
||||
|
||||
/// A 2D texture.
|
||||
pub struct Texture {
|
||||
/// The underlying MTLTexture Object.
|
||||
texture: Retained<ProtocolObject<dyn MTLTexture>>,
|
||||
|
||||
/// The width of this texture.
|
||||
pub width: usize,
|
||||
/// The height of this texture.
|
||||
pub height: usize,
|
||||
|
||||
/// Bytes per pixel for this texture.
|
||||
pub bpp: usize,
|
||||
}
|
||||
|
||||
// SAFETY: MTLTexture objects may be used from any thread (the CPU side only
|
||||
// calls `replaceRegion`, which the renderer serializes through its draw
|
||||
// mutex).
|
||||
unsafe impl Send for Texture {}
|
||||
// SAFETY: see above.
|
||||
unsafe impl Sync for Texture {}
|
||||
|
||||
impl Texture {
|
||||
/// The underlying MTLTexture.
|
||||
pub fn texture(&self) -> &ProtocolObject<dyn MTLTexture> {
|
||||
&self.texture
|
||||
}
|
||||
|
||||
/// Initialize a texture
|
||||
pub fn init(
|
||||
opts: &Options,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Option<&[u8]>,
|
||||
) -> Result<Texture, Error> {
|
||||
// Create our descriptor and set our properties
|
||||
let desc = MTLTextureDescriptor::new();
|
||||
desc.setPixelFormat(MTLPixelFormat(opts.pixel_format as usize));
|
||||
// SAFETY: plain numbers, validated when the texture is created.
|
||||
unsafe {
|
||||
desc.setWidth(width);
|
||||
desc.setHeight(height);
|
||||
}
|
||||
desc.setResourceOptions(MTLResourceOptions(opts.resource_options.bits() as usize));
|
||||
desc.setUsage(MTLTextureUsage(opts.usage.bits() as usize));
|
||||
|
||||
// Initialize
|
||||
let texture = opts
|
||||
.device
|
||||
.newTextureWithDescriptor(&desc)
|
||||
.ok_or(Error::MetalFailed)?;
|
||||
|
||||
let this = Texture {
|
||||
texture,
|
||||
width,
|
||||
height,
|
||||
bpp: opts.pixel_format.bytes_per_pixel(),
|
||||
};
|
||||
|
||||
// If we have data, we set it here.
|
||||
if let Some(d) = data {
|
||||
debug_assert_eq!(d.len(), width * height * this.bpp);
|
||||
this.replace_region(0, 0, width, height, d)?;
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Replace a region of the texture with the provided data.
|
||||
///
|
||||
/// Zig does NOT check the dimensions of the data to ensure correctness;
|
||||
/// since this is a safe function we reject data that is too short.
|
||||
pub fn replace_region(
|
||||
&self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let bytes_per_row = self.bpp * width;
|
||||
if data.len() < bytes_per_row * height {
|
||||
return Err(Error::MetalFailed);
|
||||
}
|
||||
let Some(ptr) = NonNull::new(data.as_ptr().cast_mut().cast::<c_void>()) else {
|
||||
return Err(Error::MetalFailed);
|
||||
};
|
||||
let region = MTLRegion {
|
||||
origin: MTLOrigin { x, y, z: 0 },
|
||||
size: MTLSize {
|
||||
width,
|
||||
height,
|
||||
depth: 1,
|
||||
},
|
||||
};
|
||||
// SAFETY: `ptr` points to at least `bytes_per_row * height` readable
|
||||
// bytes (checked above); Metal only reads from it.
|
||||
unsafe {
|
||||
self.texture
|
||||
.replaceRegion_mipmapLevel_withBytes_bytesPerRow(region, 0, ptr, bytes_per_row);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuTexture for Texture {
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn replace_region(
|
||||
&self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: &[u8],
|
||||
) -> Result<(), ApiError> {
|
||||
Texture::replace_region(self, x, y, width, height, data).map_err(ApiError::new)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
//! Platform-independent parts of the Metal backend (`renderer/metal/api.zig`,
|
||||
//! `metal/shaders.zig`, `metal/Texture.zig`, `Metal.zig`): raw enum values,
|
||||
//! the Metal GPU data layouts, pixel format selection, vertex attribute
|
||||
//! tables and the small pieces of math the backend does. Everything here is
|
||||
//! plain Rust so it can be unit tested on any host; `crate::metal` converts
|
||||
//! these values to `objc2-metal` types.
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use ghostty_config::types_b::AlphaBlending;
|
||||
use ghostty_font::atlas::Format as AtlasFormat;
|
||||
|
||||
use crate::{
|
||||
graphics_api::ImageTextureFormat,
|
||||
math::Mat,
|
||||
shader_types::{BgImage, CellText, Image as ImageVertex, Uniforms},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resource options
|
||||
|
||||
/// `MTLResourceOptions.CPUCacheMode`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum CpuCacheMode {
|
||||
#[default]
|
||||
Default = 0,
|
||||
WriteCombined = 1,
|
||||
}
|
||||
|
||||
/// `MTLResourceOptions.StorageMode`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum StorageMode {
|
||||
Shared = 0,
|
||||
Managed = 1,
|
||||
Private = 2,
|
||||
Memoryless = 3,
|
||||
}
|
||||
|
||||
/// `MTLResourceOptions.HazardTrackingMode`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum HazardTrackingMode {
|
||||
#[default]
|
||||
Default = 0,
|
||||
Untracked = 1,
|
||||
Tracked = 2,
|
||||
}
|
||||
|
||||
/// `MTLResourceOptions` (a `packed struct(c_ulong)` upstream: cache mode in
|
||||
/// bits 0-3, storage mode in bits 4-7, hazard tracking in bits 8-9).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ResourceOptions {
|
||||
pub cpu_cache_mode: CpuCacheMode,
|
||||
pub storage_mode: StorageMode,
|
||||
pub hazard_tracking_mode: HazardTrackingMode,
|
||||
}
|
||||
|
||||
impl ResourceOptions {
|
||||
/// "Indicate that the CPU writes to this resource but never reads it."
|
||||
pub fn write_combined(storage_mode: StorageMode) -> ResourceOptions {
|
||||
ResourceOptions {
|
||||
cpu_cache_mode: CpuCacheMode::WriteCombined,
|
||||
storage_mode,
|
||||
hazard_tracking_mode: HazardTrackingMode::Default,
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw `MTLResourceOptions` value.
|
||||
pub fn bits(self) -> u64 {
|
||||
u64::from(self.cpu_cache_mode as u8)
|
||||
| u64::from(self.storage_mode as u8) << 4
|
||||
| u64::from(self.hazard_tracking_mode as u8) << 8
|
||||
}
|
||||
}
|
||||
|
||||
/// `MTLTextureUsage` (a `packed struct(c_ulong)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct TextureUsage {
|
||||
pub shader_read: bool,
|
||||
pub shader_write: bool,
|
||||
pub render_target: bool,
|
||||
pub pixel_format_view: bool,
|
||||
pub shader_atomic: bool,
|
||||
}
|
||||
|
||||
impl TextureUsage {
|
||||
/// The raw `MTLTextureUsage` value.
|
||||
pub fn bits(self) -> u64 {
|
||||
u64::from(self.shader_read)
|
||||
| u64::from(self.shader_write) << 1
|
||||
| u64::from(self.render_target) << 2
|
||||
// bit 3 is skipped by the enum
|
||||
| u64::from(self.pixel_format_view) << 4
|
||||
| u64::from(self.shader_atomic) << 5
|
||||
}
|
||||
|
||||
/// Textures only read from a shader (atlases, images).
|
||||
pub fn shader_read_only() -> TextureUsage {
|
||||
TextureUsage {
|
||||
shader_read: true,
|
||||
..TextureUsage::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom shader textures: "require both the shader read (for when
|
||||
/// multiple shaders are chained) and render target (for the final output)
|
||||
/// usage. Disabling either of these will lead to metal validation errors
|
||||
/// in Xcode."
|
||||
pub fn custom_shader() -> TextureUsage {
|
||||
TextureUsage {
|
||||
shader_read: true,
|
||||
render_target: true,
|
||||
..TextureUsage::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default storage mode to use for resources created with a device.
|
||||
///
|
||||
/// "This is based on whether the device is a discrete GPU or not, since
|
||||
/// discrete GPUs do not have unified memory and therefore do not support the
|
||||
/// 'shared' storage mode, instead we have to use the 'managed' mode." The
|
||||
/// managed mode is not supported by iOS.
|
||||
pub fn default_storage_mode(is_ios: bool, has_unified_memory: bool) -> StorageMode {
|
||||
if is_ios || has_unified_memory {
|
||||
StorageMode::Shared
|
||||
} else {
|
||||
StorageMode::Managed
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum 2D texture size by GPU family
|
||||
/// (<https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf>).
|
||||
pub fn max_texture_size(supports_apple10: bool, supports_apple3: bool) -> u32 {
|
||||
if supports_apple10 {
|
||||
32768
|
||||
} else if supports_apple3 {
|
||||
16384
|
||||
} else {
|
||||
8192
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamp a layer's pixel size (`bounds * contentsScale`) to the maximum
|
||||
/// texture size ("we can't create a screen buffer (texture) larger than
|
||||
/// that"). Truncates like Zig's `@intFromFloat`.
|
||||
pub fn surface_size(
|
||||
bounds_width: f64,
|
||||
bounds_height: f64,
|
||||
scale: f64,
|
||||
max_texture_size: u32,
|
||||
) -> (u32, u32) {
|
||||
let w = (bounds_width * scale) as u32;
|
||||
let h = (bounds_height * scale) as u32;
|
||||
(w.min(max_texture_size), h.min(max_texture_size))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pixel formats
|
||||
|
||||
/// The subset of `MTLPixelFormat` the renderer uses, with the raw values from
|
||||
/// `api.zig`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum PixelFormat {
|
||||
R8Unorm = 10,
|
||||
R8UnormSrgb = 11,
|
||||
Rgba8Unorm = 70,
|
||||
Rgba8UnormSrgb = 71,
|
||||
Bgra8Unorm = 80,
|
||||
Bgra8UnormSrgb = 81,
|
||||
}
|
||||
|
||||
impl PixelFormat {
|
||||
/// Bytes per pixel (`Texture.bppOf`).
|
||||
pub fn bytes_per_pixel(self) -> usize {
|
||||
match self {
|
||||
PixelFormat::R8Unorm | PixelFormat::R8UnormSrgb => 1,
|
||||
PixelFormat::Rgba8Unorm
|
||||
| PixelFormat::Rgba8UnormSrgb
|
||||
| PixelFormat::Bgra8Unorm
|
||||
| PixelFormat::Bgra8UnormSrgb => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixel format of render targets, custom shader textures and pipeline
|
||||
/// color attachments.
|
||||
///
|
||||
/// "Using an `*_srgb` pixel format makes Metal gamma encode the pixels
|
||||
/// written to it *after* blending, which means we get linear alpha blending
|
||||
/// rather than gamma-incorrect blending."
|
||||
pub fn color_pixel_format(blending: AlphaBlending) -> PixelFormat {
|
||||
if blending.is_linear() {
|
||||
PixelFormat::Bgra8UnormSrgb
|
||||
} else {
|
||||
PixelFormat::Bgra8Unorm
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixel format of an image texture (`ImageTextureFormat.toPixelFormat`).
|
||||
pub fn image_pixel_format(format: ImageTextureFormat, srgb: bool) -> PixelFormat {
|
||||
match (format, srgb) {
|
||||
(ImageTextureFormat::Gray, true) => PixelFormat::R8UnormSrgb,
|
||||
(ImageTextureFormat::Gray, false) => PixelFormat::R8Unorm,
|
||||
(ImageTextureFormat::Rgba, true) => PixelFormat::Rgba8UnormSrgb,
|
||||
(ImageTextureFormat::Rgba, false) => PixelFormat::Rgba8Unorm,
|
||||
(ImageTextureFormat::Bgra, true) => PixelFormat::Bgra8UnormSrgb,
|
||||
(ImageTextureFormat::Bgra, false) => PixelFormat::Bgra8Unorm,
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixel format of a font atlas texture (`initAtlasTexture`). `None` for
|
||||
/// formats Metal doesn't support (Zig panics).
|
||||
pub fn atlas_pixel_format(format: AtlasFormat) -> Option<PixelFormat> {
|
||||
match format {
|
||||
AtlasFormat::Grayscale => Some(PixelFormat::R8Unorm),
|
||||
AtlasFormat::Bgra => Some(PixelFormat::Bgra8UnormSrgb),
|
||||
AtlasFormat::Bgr => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vertex attributes
|
||||
|
||||
/// The subset of `MTLVertexFormat` used by our vertex structs (raw values from
|
||||
/// `api.zig`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum VertexFormat {
|
||||
Uchar4 = 3,
|
||||
Ushort2 = 13,
|
||||
Short2 = 16,
|
||||
Float = 28,
|
||||
Float2 = 29,
|
||||
Float4 = 31,
|
||||
Uint2 = 37,
|
||||
Uchar = 45,
|
||||
}
|
||||
|
||||
/// One vertex attribute (what `Pipeline.autoAttribute` derives by reflection).
|
||||
/// The attribute index is the position in the table; all attributes read from
|
||||
/// buffer 0.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct VertexAttribute {
|
||||
pub format: VertexFormat,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
/// A vertex struct usable with a Metal pipeline.
|
||||
pub trait MetalVertex: Pod {
|
||||
const ATTRIBUTES: &'static [VertexAttribute];
|
||||
}
|
||||
|
||||
macro_rules! attr {
|
||||
($ty:ty, $field:ident, $format:expr) => {
|
||||
VertexAttribute {
|
||||
format: $format,
|
||||
offset: core::mem::offset_of!($ty, $field),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl MetalVertex for CellText {
|
||||
const ATTRIBUTES: &'static [VertexAttribute] = &[
|
||||
attr!(CellText, glyph_pos, VertexFormat::Uint2),
|
||||
attr!(CellText, glyph_size, VertexFormat::Uint2),
|
||||
attr!(CellText, bearings, VertexFormat::Short2),
|
||||
attr!(CellText, grid_pos, VertexFormat::Ushort2),
|
||||
attr!(CellText, color, VertexFormat::Uchar4),
|
||||
// `atlas` is an enum(u8): its tag type.
|
||||
attr!(CellText, atlas, VertexFormat::Uchar),
|
||||
// `bools` is a packed struct(u8): its backing integer.
|
||||
attr!(CellText, bools, VertexFormat::Uchar),
|
||||
];
|
||||
}
|
||||
|
||||
impl MetalVertex for ImageVertex {
|
||||
const ATTRIBUTES: &'static [VertexAttribute] = &[
|
||||
attr!(ImageVertex, grid_pos, VertexFormat::Float2),
|
||||
attr!(ImageVertex, cell_offset, VertexFormat::Float2),
|
||||
attr!(ImageVertex, source_rect, VertexFormat::Float4),
|
||||
attr!(ImageVertex, dest_size, VertexFormat::Float2),
|
||||
];
|
||||
}
|
||||
|
||||
impl MetalVertex for BgImage {
|
||||
const ATTRIBUTES: &'static [VertexAttribute] = &[
|
||||
attr!(BgImage, opacity, VertexFormat::Float),
|
||||
// `info` is a packed struct(u8): its backing integer.
|
||||
attr!(BgImage, info, VertexFormat::Uchar),
|
||||
];
|
||||
}
|
||||
|
||||
/// `MTLVertexStepFunction`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum VertexStepFunction {
|
||||
Constant = 0,
|
||||
PerVertex = 1,
|
||||
PerInstance = 2,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Blending
|
||||
|
||||
/// The color attachment blend state of a pipeline
|
||||
/// ("We always use premultiplied alpha blending for now.").
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BlendState {
|
||||
pub rgb_operation: BlendOperation,
|
||||
pub alpha_operation: BlendOperation,
|
||||
pub source_rgb: BlendFactor,
|
||||
pub source_alpha: BlendFactor,
|
||||
pub destination_rgb: BlendFactor,
|
||||
pub destination_alpha: BlendFactor,
|
||||
}
|
||||
|
||||
/// `MTLBlendOperation` (subset).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum BlendOperation {
|
||||
Add = 0,
|
||||
}
|
||||
|
||||
/// `MTLBlendFactor` (subset).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
pub enum BlendFactor {
|
||||
One = 1,
|
||||
OneMinusSourceAlpha = 5,
|
||||
}
|
||||
|
||||
/// The blend state used by every blending pipeline.
|
||||
pub const PREMULTIPLIED_ALPHA: BlendState = BlendState {
|
||||
rgb_operation: BlendOperation::Add,
|
||||
alpha_operation: BlendOperation::Add,
|
||||
source_rgb: BlendFactor::One,
|
||||
source_alpha: BlendFactor::One,
|
||||
destination_rgb: BlendFactor::OneMinusSourceAlpha,
|
||||
destination_alpha: BlendFactor::OneMinusSourceAlpha,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render pass buffer slots
|
||||
|
||||
/// The Metal buffer index for entry `i` of `Step.buffers`
|
||||
/// (`RenderPass.step`): index 0 is the vertex buffer, and the rest start at
|
||||
/// index 2 "so that we can use index 1 for the uniforms if present".
|
||||
pub fn buffer_index(i: usize) -> usize {
|
||||
if i == 0 { 0 } else { i + 1 }
|
||||
}
|
||||
|
||||
/// The Metal buffer index of the uniforms.
|
||||
pub const UNIFORMS_BUFFER_INDEX: usize = 1;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Uniforms
|
||||
|
||||
/// The uniforms that are passed to our Metal shaders (`metal/shaders.zig`
|
||||
/// `Uniforms`, an `extern struct` with the alignments of the MSL struct).
|
||||
///
|
||||
/// This is the same layout as the OpenGL std140 struct except for `bools`,
|
||||
/// which is four separate bytes instead of a bit mask, and the width of
|
||||
/// `padding_extend`.
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct MetalUniforms {
|
||||
pub projection_matrix: Mat,
|
||||
pub screen_size: [f32; 2],
|
||||
pub cell_size: [f32; 2],
|
||||
pub grid_size: [u16; 2],
|
||||
pub pad0: [u8; 12],
|
||||
pub grid_padding: [f32; 4],
|
||||
/// Bit mask: left, right, up, down (LSB first).
|
||||
pub padding_extend: u8,
|
||||
pub pad1: [u8; 3],
|
||||
pub min_contrast: f32,
|
||||
pub cursor_pos: [u16; 2],
|
||||
pub cursor_color: [u8; 4],
|
||||
pub bg_color: [u8; 4],
|
||||
pub cursor_wide: u8,
|
||||
pub use_display_p3: u8,
|
||||
pub use_linear_blending: u8,
|
||||
pub use_linear_correction: u8,
|
||||
pub pad2: [u8; 8],
|
||||
}
|
||||
|
||||
impl From<&Uniforms> for MetalUniforms {
|
||||
fn from(u: &Uniforms) -> MetalUniforms {
|
||||
use crate::shader_types::Bools;
|
||||
MetalUniforms {
|
||||
projection_matrix: u.projection_matrix,
|
||||
screen_size: u.screen_size,
|
||||
cell_size: u.cell_size,
|
||||
grid_size: u.grid_size,
|
||||
pad0: [0; 12],
|
||||
grid_padding: u.grid_padding,
|
||||
padding_extend: (u.padding_extend & 0xFF) as u8,
|
||||
pad1: [0; 3],
|
||||
min_contrast: u.min_contrast,
|
||||
cursor_pos: u.cursor_pos,
|
||||
cursor_color: u.cursor_color,
|
||||
bg_color: u.bg_color,
|
||||
cursor_wide: u8::from(u.bools & Bools::CURSOR_WIDE != 0),
|
||||
use_display_p3: u8::from(u.bools & Bools::USE_DISPLAY_P3 != 0),
|
||||
use_linear_blending: u8::from(u.bools & Bools::USE_LINEAR_BLENDING != 0),
|
||||
use_linear_correction: u8::from(u.bools & Bools::USE_LINEAR_CORRECTION != 0),
|
||||
pad2: [0; 8],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom shaders
|
||||
|
||||
/// The entry point of the fragment function spirv-cross (and naga, see
|
||||
/// `shadertoy::msl_from_compiled`) generates for custom shaders.
|
||||
pub const POST_FRAGMENT_FN: &str = crate::shadertoy::MSL_ENTRY_POINT;
|
||||
|
||||
/// The vertex function post-processing pipelines share with the built-in
|
||||
/// shaders.
|
||||
pub const FULL_SCREEN_VERTEX_FN: &str = "full_screen_vertex";
|
||||
|
||||
/// The built-in pipelines (`pipeline_descs` in `metal/shaders.zig`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PipelineDesc {
|
||||
pub name: &'static str,
|
||||
pub vertex_fn: &'static str,
|
||||
pub fragment_fn: &'static str,
|
||||
pub step_fn: VertexStepFunction,
|
||||
pub blending_enabled: bool,
|
||||
/// Whether the pipeline has a vertex descriptor (vertex attributes).
|
||||
pub has_vertex_attributes: bool,
|
||||
}
|
||||
|
||||
pub const PIPELINE_DESCS: [PipelineDesc; 5] = [
|
||||
PipelineDesc {
|
||||
name: "bg_color",
|
||||
vertex_fn: "full_screen_vertex",
|
||||
fragment_fn: "bg_color_fragment",
|
||||
step_fn: VertexStepFunction::PerVertex,
|
||||
blending_enabled: false,
|
||||
has_vertex_attributes: false,
|
||||
},
|
||||
PipelineDesc {
|
||||
name: "cell_bg",
|
||||
vertex_fn: "full_screen_vertex",
|
||||
fragment_fn: "cell_bg_fragment",
|
||||
step_fn: VertexStepFunction::PerVertex,
|
||||
blending_enabled: true,
|
||||
has_vertex_attributes: false,
|
||||
},
|
||||
PipelineDesc {
|
||||
name: "cell_text",
|
||||
vertex_fn: "cell_text_vertex",
|
||||
fragment_fn: "cell_text_fragment",
|
||||
step_fn: VertexStepFunction::PerInstance,
|
||||
blending_enabled: true,
|
||||
has_vertex_attributes: true,
|
||||
},
|
||||
PipelineDesc {
|
||||
name: "image",
|
||||
vertex_fn: "image_vertex",
|
||||
fragment_fn: "image_fragment",
|
||||
step_fn: VertexStepFunction::PerInstance,
|
||||
blending_enabled: true,
|
||||
has_vertex_attributes: true,
|
||||
},
|
||||
PipelineDesc {
|
||||
name: "bg_image",
|
||||
vertex_fn: "bg_image_vertex",
|
||||
fragment_fn: "bg_image_fragment",
|
||||
step_fn: VertexStepFunction::PerInstance,
|
||||
blending_enabled: true,
|
||||
has_vertex_attributes: true,
|
||||
},
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::mem::{offset_of, size_of};
|
||||
|
||||
use super::*;
|
||||
use crate::shader_types::{Bools, PaddingExtend};
|
||||
|
||||
#[test]
|
||||
fn resource_options_bits() {
|
||||
// MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeShared
|
||||
assert_eq!(
|
||||
ResourceOptions::write_combined(StorageMode::Shared).bits(),
|
||||
1
|
||||
);
|
||||
// ... | MTLResourceStorageModeManaged (1 << 4)
|
||||
assert_eq!(
|
||||
ResourceOptions::write_combined(StorageMode::Managed).bits(),
|
||||
0x11
|
||||
);
|
||||
let private = ResourceOptions {
|
||||
cpu_cache_mode: CpuCacheMode::Default,
|
||||
storage_mode: StorageMode::Private,
|
||||
hazard_tracking_mode: HazardTrackingMode::Tracked,
|
||||
};
|
||||
assert_eq!(private.bits(), (2 << 4) | (2 << 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn texture_usage_bits() {
|
||||
assert_eq!(TextureUsage::shader_read_only().bits(), 1);
|
||||
assert_eq!(TextureUsage::custom_shader().bits(), 1 | 4);
|
||||
let all = TextureUsage {
|
||||
shader_read: true,
|
||||
shader_write: true,
|
||||
render_target: true,
|
||||
pixel_format_view: true,
|
||||
shader_atomic: true,
|
||||
};
|
||||
// The enum skips from 4 to 16.
|
||||
assert_eq!(all.bits(), 1 | 2 | 4 | 16 | 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_mode_selection() {
|
||||
assert_eq!(default_storage_mode(false, true), StorageMode::Shared);
|
||||
assert_eq!(default_storage_mode(false, false), StorageMode::Managed);
|
||||
// Managed storage does not exist on iOS.
|
||||
assert_eq!(default_storage_mode(true, false), StorageMode::Shared);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_texture_size_by_family() {
|
||||
assert_eq!(max_texture_size(true, true), 32768);
|
||||
assert_eq!(max_texture_size(false, true), 16384);
|
||||
assert_eq!(max_texture_size(false, false), 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_size_clamps_and_truncates() {
|
||||
assert_eq!(surface_size(800.0, 600.0, 2.0, 16384), (1600, 1200));
|
||||
assert_eq!(surface_size(800.5, 600.5, 1.5, 16384), (1200, 900));
|
||||
assert_eq!(surface_size(10000.0, 10.0, 2.0, 8192), (8192, 20));
|
||||
assert_eq!(surface_size(0.0, 0.0, 2.0, 8192), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_format_selection() {
|
||||
assert_eq!(
|
||||
color_pixel_format(AlphaBlending::Native),
|
||||
PixelFormat::Bgra8Unorm
|
||||
);
|
||||
assert_eq!(
|
||||
color_pixel_format(AlphaBlending::Linear),
|
||||
PixelFormat::Bgra8UnormSrgb
|
||||
);
|
||||
assert_eq!(
|
||||
color_pixel_format(AlphaBlending::LinearCorrected),
|
||||
PixelFormat::Bgra8UnormSrgb
|
||||
);
|
||||
assert_eq!(
|
||||
image_pixel_format(ImageTextureFormat::Gray, true),
|
||||
PixelFormat::R8UnormSrgb
|
||||
);
|
||||
assert_eq!(
|
||||
image_pixel_format(ImageTextureFormat::Rgba, false),
|
||||
PixelFormat::Rgba8Unorm
|
||||
);
|
||||
assert_eq!(
|
||||
image_pixel_format(ImageTextureFormat::Bgra, true),
|
||||
PixelFormat::Bgra8UnormSrgb
|
||||
);
|
||||
assert_eq!(
|
||||
atlas_pixel_format(AtlasFormat::Grayscale),
|
||||
Some(PixelFormat::R8Unorm)
|
||||
);
|
||||
assert_eq!(
|
||||
atlas_pixel_format(AtlasFormat::Bgra),
|
||||
Some(PixelFormat::Bgra8UnormSrgb)
|
||||
);
|
||||
assert_eq!(atlas_pixel_format(AtlasFormat::Bgr), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_format_raw_values_match_metal() {
|
||||
// MTLPixelFormat values (api.zig).
|
||||
assert_eq!(PixelFormat::R8Unorm as u64, 10);
|
||||
assert_eq!(PixelFormat::Rgba8Unorm as u64, 70);
|
||||
assert_eq!(PixelFormat::Bgra8Unorm as u64, 80);
|
||||
assert_eq!(PixelFormat::Bgra8UnormSrgb as u64, 81);
|
||||
assert_eq!(PixelFormat::Bgra8Unorm.bytes_per_pixel(), 4);
|
||||
assert_eq!(PixelFormat::R8UnormSrgb.bytes_per_pixel(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertex_attribute_tables() {
|
||||
let cell_text: Vec<_> = CellText::ATTRIBUTES
|
||||
.iter()
|
||||
.map(|a| (a.format, a.offset))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
cell_text,
|
||||
[
|
||||
(VertexFormat::Uint2, 0),
|
||||
(VertexFormat::Uint2, 8),
|
||||
(VertexFormat::Short2, 16),
|
||||
(VertexFormat::Ushort2, 20),
|
||||
(VertexFormat::Uchar4, 24),
|
||||
(VertexFormat::Uchar, 28),
|
||||
(VertexFormat::Uchar, 29),
|
||||
]
|
||||
);
|
||||
let image: Vec<_> = ImageVertex::ATTRIBUTES
|
||||
.iter()
|
||||
.map(|a| (a.format, a.offset))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
image,
|
||||
[
|
||||
(VertexFormat::Float2, 0),
|
||||
(VertexFormat::Float2, 8),
|
||||
(VertexFormat::Float4, 16),
|
||||
(VertexFormat::Float2, 32),
|
||||
]
|
||||
);
|
||||
let bg: Vec<_> = BgImage::ATTRIBUTES
|
||||
.iter()
|
||||
.map(|a| (a.format, a.offset))
|
||||
.collect();
|
||||
assert_eq!(bg, [(VertexFormat::Float, 0), (VertexFormat::Uchar, 4)]);
|
||||
|
||||
// MTLVertexFormat raw values.
|
||||
assert_eq!(VertexFormat::Uchar4 as u64, 3);
|
||||
assert_eq!(VertexFormat::Ushort2 as u64, 13);
|
||||
assert_eq!(VertexFormat::Short2 as u64, 16);
|
||||
assert_eq!(VertexFormat::Float as u64, 28);
|
||||
assert_eq!(VertexFormat::Float2 as u64, 29);
|
||||
assert_eq!(VertexFormat::Float4 as u64, 31);
|
||||
assert_eq!(VertexFormat::Uint2 as u64, 37);
|
||||
assert_eq!(VertexFormat::Uchar as u64, 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_slots() {
|
||||
// Vertex buffer at 0, uniforms at 1, the rest start at 2.
|
||||
assert_eq!(buffer_index(0), 0);
|
||||
assert_eq!(buffer_index(1), 2);
|
||||
assert_eq!(buffer_index(2), 3);
|
||||
assert_eq!(UNIFORMS_BUFFER_INDEX, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_state_is_premultiplied() {
|
||||
assert_eq!(PREMULTIPLIED_ALPHA.source_rgb as u64, 1);
|
||||
assert_eq!(PREMULTIPLIED_ALPHA.destination_rgb as u64, 5);
|
||||
assert_eq!(PREMULTIPLIED_ALPHA.rgb_operation as u64, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metal_uniforms_layout_matches_msl() {
|
||||
// The MSL `Uniforms` struct in shaders/shaders.metal.
|
||||
assert_eq!(offset_of!(MetalUniforms, projection_matrix), 0);
|
||||
assert_eq!(offset_of!(MetalUniforms, screen_size), 64);
|
||||
assert_eq!(offset_of!(MetalUniforms, cell_size), 72);
|
||||
assert_eq!(offset_of!(MetalUniforms, grid_size), 80);
|
||||
assert_eq!(offset_of!(MetalUniforms, grid_padding), 96);
|
||||
assert_eq!(offset_of!(MetalUniforms, padding_extend), 112);
|
||||
assert_eq!(offset_of!(MetalUniforms, min_contrast), 116);
|
||||
assert_eq!(offset_of!(MetalUniforms, cursor_pos), 120);
|
||||
assert_eq!(offset_of!(MetalUniforms, cursor_color), 124);
|
||||
assert_eq!(offset_of!(MetalUniforms, bg_color), 128);
|
||||
assert_eq!(offset_of!(MetalUniforms, cursor_wide), 132);
|
||||
assert_eq!(offset_of!(MetalUniforms, use_linear_correction), 135);
|
||||
assert_eq!(size_of::<MetalUniforms>(), 144);
|
||||
// Same size as the OpenGL std140 block.
|
||||
assert_eq!(size_of::<MetalUniforms>(), size_of::<Uniforms>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metal_uniforms_from_common() {
|
||||
let common = Uniforms {
|
||||
screen_size: [800.0, 600.0],
|
||||
padding_extend: PaddingExtend {
|
||||
left: true,
|
||||
right: false,
|
||||
up: false,
|
||||
down: true,
|
||||
}
|
||||
.to_bits(),
|
||||
min_contrast: 3.0,
|
||||
cursor_pos: [4, 5],
|
||||
bg_color: [1, 2, 3, 4],
|
||||
bools: Bools {
|
||||
cursor_wide: true,
|
||||
use_display_p3: false,
|
||||
use_linear_blending: true,
|
||||
use_linear_correction: false,
|
||||
}
|
||||
.to_bits(),
|
||||
..Uniforms::default()
|
||||
};
|
||||
let m = MetalUniforms::from(&common);
|
||||
assert_eq!(m.screen_size, [800.0, 600.0]);
|
||||
assert_eq!(m.padding_extend, 0b1001);
|
||||
assert!((m.min_contrast - 3.0).abs() < f32::EPSILON);
|
||||
assert_eq!(m.cursor_pos, [4, 5]);
|
||||
assert_eq!(m.bg_color, [1, 2, 3, 4]);
|
||||
let raw: &[u8] = bytemuck::bytes_of(&m);
|
||||
// cursor_wide, use_display_p3, use_linear_blending, use_linear_correction
|
||||
assert_eq!(&raw[132..136], &[1, 0, 1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_descs_match_shader_functions() {
|
||||
let names: Vec<_> = PIPELINE_DESCS.iter().map(|d| d.name).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
["bg_color", "cell_bg", "cell_text", "image", "bg_image"]
|
||||
);
|
||||
// Every function exists in shaders.metal.
|
||||
let src = include_str!("../shaders/shaders.metal");
|
||||
for d in &PIPELINE_DESCS {
|
||||
assert!(
|
||||
src.contains(&format!("{}(", d.vertex_fn)),
|
||||
"{}",
|
||||
d.vertex_fn
|
||||
);
|
||||
assert!(
|
||||
src.contains(&format!("{}(", d.fragment_fn)),
|
||||
"{}",
|
||||
d.fragment_fn
|
||||
);
|
||||
}
|
||||
assert!(src.contains(FULL_SCREEN_VERTEX_FN));
|
||||
// Instanced pipelines have vertex attributes.
|
||||
for d in &PIPELINE_DESCS {
|
||||
assert_eq!(
|
||||
d.has_vertex_attributes,
|
||||
d.step_fn == VertexStepFunction::PerInstance
|
||||
);
|
||||
}
|
||||
assert!(!PIPELINE_DESCS[0].blending_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msl_uniforms_struct_matches_rust() {
|
||||
// Guard the shader <-> Rust contract: the MSL struct has these fields
|
||||
// in this order.
|
||||
let src = include_str!("../shaders/shaders.metal");
|
||||
let start = src.find("struct Uniforms {").unwrap();
|
||||
let end = start + src[start..].find("};").unwrap();
|
||||
let fields: Vec<&str> = src[start..end]
|
||||
.lines()
|
||||
.skip(1)
|
||||
.map(|l| l.trim().trim_end_matches(';'))
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| l.rsplit(' ').next().unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
fields,
|
||||
[
|
||||
"projection_matrix",
|
||||
"screen_size",
|
||||
"cell_size",
|
||||
"grid_size",
|
||||
"grid_padding",
|
||||
"padding_extend",
|
||||
"min_contrast",
|
||||
"cursor_pos",
|
||||
"cursor_color",
|
||||
"bg_color",
|
||||
"cursor_wide",
|
||||
"use_display_p3",
|
||||
"use_linear_blending",
|
||||
"use_linear_correction",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! `impl GraphicsApi for OpenGL`: adapts the OpenGL backend to the interface
|
||||
//! the generic renderer is written against (see [`crate::graphics_api`]).
|
||||
//!
|
||||
//! The adapter delegates to the inherent OpenGL API (which keeps its richer,
|
||||
//! GL-typed signatures for the apprt and the integration tests). Inherent
|
||||
//! methods are always called with path syntax so they win over the trait
|
||||
//! methods of the same name.
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytemuck::Pod;
|
||||
use ghostty_config::types_b::AlphaBlending;
|
||||
use ghostty_font::atlas::Atlas;
|
||||
|
||||
use super::{
|
||||
Buffer,
|
||||
Frame,
|
||||
OpenGL,
|
||||
Pipeline,
|
||||
RenderPass,
|
||||
Sampler,
|
||||
Target,
|
||||
Texture,
|
||||
gl::RawBuffer,
|
||||
shaders::Shaders,
|
||||
};
|
||||
use crate::{
|
||||
backend::Health,
|
||||
graphics_api::{
|
||||
ApiError,
|
||||
Attachment,
|
||||
FrameCompletion,
|
||||
GpuBuffer,
|
||||
GpuFrame,
|
||||
GpuRenderPass,
|
||||
GpuShaders,
|
||||
GpuTarget,
|
||||
GpuTexture,
|
||||
GraphicsApi,
|
||||
Step,
|
||||
TextureKind,
|
||||
},
|
||||
shader_types::Uniforms,
|
||||
shadertoy,
|
||||
};
|
||||
|
||||
impl GraphicsApi for OpenGL {
|
||||
/// Custom shaders are converted to GLSL for OpenGL.
|
||||
const CUSTOM_SHADER_TARGET: shadertoy::Target = shadertoy::Target::Glsl;
|
||||
/// The fragCoord for OpenGL shaders is +Y = up.
|
||||
const CUSTOM_SHADER_Y_IS_DOWN: bool = false;
|
||||
/// Because OpenGL's frame completion is always
|
||||
/// sync, we have no need for multi-buffering.
|
||||
const SWAP_CHAIN_COUNT: usize = 1;
|
||||
|
||||
type Target = Target;
|
||||
type Texture = Texture;
|
||||
type Sampler = Sampler;
|
||||
type Buffer<T: Pod> = Buffer<T>;
|
||||
type BufferRef = RawBuffer;
|
||||
type Pipeline = Pipeline;
|
||||
type Shaders = Shaders;
|
||||
type GpuUniforms = Uniforms;
|
||||
type Frame = GlFrame;
|
||||
type RenderPass<'a> = RenderPass<'a>;
|
||||
|
||||
fn gpu_uniforms(uniforms: &Uniforms) -> Uniforms {
|
||||
*uniforms
|
||||
}
|
||||
|
||||
fn blending(&self) -> AlphaBlending {
|
||||
self.blending
|
||||
}
|
||||
|
||||
fn set_blending(&mut self, blending: AlphaBlending) {
|
||||
self.blending = blending;
|
||||
}
|
||||
|
||||
fn init_shaders(&self, custom_shaders: &[String]) -> Result<Shaders, ApiError> {
|
||||
OpenGL::init_shaders(self, custom_shaders).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn surface_size(&self) -> Result<(u32, u32), ApiError> {
|
||||
OpenGL::surface_size(self).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_target(&self, width: usize, height: usize) -> Result<Target, ApiError> {
|
||||
OpenGL::init_target(self, width, height).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_buffer<T: Pod>(&self, len: usize) -> Result<Buffer<T>, ApiError> {
|
||||
let gl = self.gl_or_err().map_err(ApiError::new)?;
|
||||
Buffer::init(gl, self.buffer_options(), len).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_buffer_fill<T: Pod>(&self, data: &[T]) -> Result<Buffer<T>, ApiError> {
|
||||
let gl = self.gl_or_err().map_err(ApiError::new)?;
|
||||
Buffer::init_fill(gl, self.buffer_options(), data).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_texture(
|
||||
&self,
|
||||
kind: TextureKind,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: Option<&[u8]>,
|
||||
) -> Result<Texture, ApiError> {
|
||||
let gl = self.gl_or_err().map_err(ApiError::new)?;
|
||||
let opts = match kind {
|
||||
TextureKind::CustomShader => self.texture_options(),
|
||||
TextureKind::Image { format, srgb } => self.image_texture_options(format, srgb),
|
||||
};
|
||||
Texture::init(gl, opts, width, height, data).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_atlas_texture(&self, atlas: &Atlas) -> Result<Texture, ApiError> {
|
||||
OpenGL::init_atlas_texture(self, atlas).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn init_sampler(&self) -> Result<Sampler, ApiError> {
|
||||
let gl = self.gl_or_err().map_err(ApiError::new)?;
|
||||
Sampler::init(gl, self.sampler_options()).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn begin_frame(&self, target: Arc<Target>) -> GlFrame {
|
||||
GlFrame(OpenGL::begin_frame(self, target))
|
||||
}
|
||||
|
||||
fn present_last_target(&self) -> Result<(), ApiError> {
|
||||
OpenGL::present_last_target(self).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn clear_last_target(&self) {
|
||||
OpenGL::clear_last_target(self);
|
||||
}
|
||||
|
||||
fn draw_frame_start(&mut self) {
|
||||
OpenGL::draw_frame_start(self);
|
||||
}
|
||||
|
||||
fn draw_frame_end(&mut self) {
|
||||
OpenGL::draw_frame_end(self);
|
||||
}
|
||||
|
||||
fn thread_enter(&self) {
|
||||
OpenGL::thread_enter(self);
|
||||
}
|
||||
|
||||
fn thread_exit(&self) {
|
||||
OpenGL::thread_exit(self);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Pod> GpuBuffer<OpenGL, T> for Buffer<T> {
|
||||
fn raw(&self) -> RawBuffer {
|
||||
Buffer::raw(self)
|
||||
}
|
||||
|
||||
fn sync(&mut self, data: &[T]) -> Result<(), ApiError> {
|
||||
Buffer::sync(self, data).map_err(ApiError::new)
|
||||
}
|
||||
|
||||
fn sync_from_slices(&mut self, lists: &[&[T]]) -> Result<usize, ApiError> {
|
||||
Buffer::sync_from_slices(self, lists).map_err(ApiError::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuTexture for Texture {
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn replace_region(
|
||||
&self,
|
||||
x: usize,
|
||||
y: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
data: &[u8],
|
||||
) -> Result<(), ApiError> {
|
||||
Texture::replace_region(self, x, y, width, height, data).map_err(ApiError::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuTarget for Target {
|
||||
fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
}
|
||||
|
||||
impl GpuShaders<OpenGL> for Shaders {
|
||||
fn bg_color(&self) -> &Pipeline {
|
||||
&self.bg_color
|
||||
}
|
||||
|
||||
fn cell_bg(&self) -> &Pipeline {
|
||||
&self.cell_bg
|
||||
}
|
||||
|
||||
fn cell_text(&self) -> &Pipeline {
|
||||
&self.cell_text
|
||||
}
|
||||
|
||||
fn image(&self) -> &Pipeline {
|
||||
&self.image
|
||||
}
|
||||
|
||||
fn bg_image(&self) -> &Pipeline {
|
||||
&self.bg_image
|
||||
}
|
||||
|
||||
fn post_pipelines(&self) -> &[Pipeline] {
|
||||
&self.post_pipelines
|
||||
}
|
||||
}
|
||||
|
||||
/// An OpenGL [`Frame`] as seen by the generic renderer.
|
||||
pub struct GlFrame(Frame);
|
||||
|
||||
impl GpuFrame<OpenGL> for GlFrame {
|
||||
fn render_pass<'a>(&'a self, attachments: &'a [Attachment<'a, OpenGL>]) -> RenderPass<'a>
|
||||
where
|
||||
OpenGL: 'a,
|
||||
{
|
||||
self.0.render_pass(attachments)
|
||||
}
|
||||
|
||||
fn complete(self, sync: bool, completion: FrameCompletion) {
|
||||
let health: Health = self.0.complete(sync);
|
||||
completion(health);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GpuRenderPass<'a, OpenGL> for RenderPass<'a> {
|
||||
fn step(&mut self, step: &Step<'_, OpenGL>) {
|
||||
RenderPass::step(self, step);
|
||||
}
|
||||
|
||||
fn complete(self) {
|
||||
RenderPass::complete(self);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ pub struct Buffer<T: Pod> {
|
||||
/// Note this is the number of `T`s, not the size in bytes.
|
||||
pub len: usize,
|
||||
|
||||
_marker: PhantomData<T>,
|
||||
_marker: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T: Pod> Buffer<T> {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{
|
||||
LastTarget,
|
||||
OpenGL,
|
||||
gl::Gl,
|
||||
render_pass::{Attachment, Options as RenderPassOptions, RenderPass},
|
||||
target::Target,
|
||||
};
|
||||
@@ -15,26 +17,31 @@ pub struct Options {}
|
||||
/// A frame being encoded.
|
||||
///
|
||||
/// Upstream holds `*Renderer` and calls `renderer.frameCompleted(health)`
|
||||
/// after presenting; here the frame borrows the graphics API and
|
||||
/// [`Frame::complete`] returns the [`Health`] for the renderer to report.
|
||||
pub struct Frame<'a> {
|
||||
api: &'a mut OpenGL,
|
||||
/// after presenting; here [`Frame::complete`] returns the [`Health`] for the
|
||||
/// renderer to report. The frame holds its own handles (GL context and the
|
||||
/// API's last-presented-target slot) rather than borrowing the API.
|
||||
pub struct Frame {
|
||||
gl: Gl,
|
||||
last_target: LastTarget,
|
||||
target: Arc<Target>,
|
||||
}
|
||||
|
||||
impl<'a> Frame<'a> {
|
||||
impl Frame {
|
||||
/// Begin encoding a frame.
|
||||
///
|
||||
/// The target is presented via the provided API when completed.
|
||||
pub fn begin(_opts: Options, api: &'a mut OpenGL, target: Arc<Target>) -> Frame<'a> {
|
||||
Frame { api, target }
|
||||
pub fn begin(_opts: Options, api: &OpenGL, target: Arc<Target>) -> Frame {
|
||||
Frame {
|
||||
gl: api.gl().expect("OpenGL context not initialized").clone(),
|
||||
last_target: api.last_target_slot(),
|
||||
target,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a render pass to this frame with the provided attachments.
|
||||
/// Returns a RenderPass which allows render steps to be added.
|
||||
pub fn render_pass<'b>(&self, attachments: &'b [Attachment<'b>]) -> RenderPass<'b> {
|
||||
let gl = self.api.gl().expect("OpenGL context not initialized");
|
||||
RenderPass::begin(gl, RenderPassOptions { attachments })
|
||||
RenderPass::begin(&self.gl, RenderPassOptions { attachments })
|
||||
}
|
||||
|
||||
/// Complete this frame and present the target.
|
||||
@@ -43,13 +50,10 @@ impl<'a> Frame<'a> {
|
||||
///
|
||||
/// NOTE: For OpenGL, `sync` is ignored and we always block.
|
||||
pub fn complete(self, _sync: bool) -> Health {
|
||||
let Some(gl) = self.api.gl().cloned() else {
|
||||
return Health::Unhealthy;
|
||||
};
|
||||
gl.finish();
|
||||
self.gl.finish();
|
||||
|
||||
// If there are any GL errors, consider the frame unhealthy.
|
||||
let health = if gl.get_error().is_ok() {
|
||||
let health = if self.gl.get_error().is_ok() {
|
||||
Health::Healthy
|
||||
} else {
|
||||
Health::Unhealthy
|
||||
@@ -57,7 +61,7 @@ impl<'a> Frame<'a> {
|
||||
|
||||
// If the frame is healthy, present it.
|
||||
if health == Health::Healthy
|
||||
&& let Err(err) = self.api.present(&self.target)
|
||||
&& let Err(err) = super::present_target(&self.gl, &self.last_target, &self.target)
|
||||
{
|
||||
log::error!("Failed to present render target: err={err}");
|
||||
}
|
||||
|
||||
@@ -160,14 +160,17 @@ pub enum Wrap {
|
||||
}
|
||||
|
||||
/// Primitive types we support drawing (same subset as Metal).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum Primitive {
|
||||
Point = glow::POINTS,
|
||||
Line = glow::LINES,
|
||||
LineStrip = glow::LINE_STRIP,
|
||||
Triangle = glow::TRIANGLES,
|
||||
TriangleStrip = glow::TRIANGLE_STRIP,
|
||||
pub use crate::graphics_api::Primitive;
|
||||
|
||||
/// The GL draw mode of a [`Primitive`].
|
||||
fn gl_mode(mode: Primitive) -> u32 {
|
||||
match mode {
|
||||
Primitive::Point => glow::POINTS,
|
||||
Primitive::Line => glow::LINES,
|
||||
Primitive::LineStrip => glow::LINE_STRIP,
|
||||
Primitive::Triangle => glow::TRIANGLES,
|
||||
Primitive::TriangleStrip => glow::TRIANGLE_STRIP,
|
||||
}
|
||||
}
|
||||
|
||||
/// Framebuffer binding targets.
|
||||
@@ -341,7 +344,7 @@ impl Gl {
|
||||
// SAFETY: see module docs; vertex data comes from bound GL buffers.
|
||||
unsafe {
|
||||
self.ctx
|
||||
.draw_arrays_instanced(mode as u32, first, count, instances);
|
||||
.draw_arrays_instanced(gl_mode(mode), first, count, instances);
|
||||
}
|
||||
self.get_error()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! closure (see [`OpenGL::prepare_context`]) by the apprt once the context is
|
||||
//! current, and everything afterwards goes through the [`Gl`] handle.
|
||||
|
||||
pub mod api;
|
||||
pub mod buffer;
|
||||
pub mod frame;
|
||||
#[allow(unsafe_code)]
|
||||
@@ -16,7 +17,10 @@ pub mod shaders;
|
||||
pub mod target;
|
||||
pub mod texture;
|
||||
|
||||
use std::{ffi::c_void, sync::Arc};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
sync::{Arc, Mutex, PoisonError},
|
||||
};
|
||||
|
||||
use ghostty_font::atlas::{Atlas, Format as AtlasFormat};
|
||||
|
||||
@@ -32,6 +36,7 @@ use self::gl::{
|
||||
Wrap,
|
||||
};
|
||||
pub use self::{
|
||||
api::GlFrame,
|
||||
buffer::Buffer,
|
||||
frame::Frame,
|
||||
gl::Gl,
|
||||
@@ -43,8 +48,6 @@ pub use self::{
|
||||
};
|
||||
use crate::shadertoy;
|
||||
|
||||
pub type GraphicsApi = OpenGL;
|
||||
|
||||
/// Custom shaders are converted to GLSL for OpenGL.
|
||||
pub const CUSTOM_SHADER_TARGET: shadertoy::Target = shadertoy::Target::Glsl;
|
||||
/// The fragCoord for OpenGL shaders is +Y = up.
|
||||
@@ -85,27 +88,54 @@ pub struct OpenGL {
|
||||
pub blending: AlphaBlending,
|
||||
|
||||
/// The most recently presented target, in case we need to present it again.
|
||||
last_target: Option<Arc<Target>>,
|
||||
/// Shared with in-flight [`Frame`]s, which record what they present.
|
||||
last_target: LastTarget,
|
||||
}
|
||||
|
||||
/// Pixel format for image texture options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImageTextureFormat {
|
||||
/// 1 byte per pixel grayscale.
|
||||
Gray,
|
||||
/// 4 bytes per pixel RGBA.
|
||||
Rgba,
|
||||
/// 4 bytes per pixel BGRA.
|
||||
Bgra,
|
||||
/// Slot holding the most recently presented target.
|
||||
pub type LastTarget = Arc<Mutex<Option<Arc<Target>>>>;
|
||||
|
||||
/// Present `target` by blitting it to the default framebuffer, remembering it
|
||||
/// in `last_target` in case we need to repeat it.
|
||||
fn present_target(gl: &Gl, last_target: &LastTarget, target: &Arc<Target>) -> Result<(), Error> {
|
||||
// In order to present a target we blit it to the default framebuffer.
|
||||
|
||||
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
|
||||
// values may be linearized as they're copied, but even though the draw
|
||||
// framebuffer has a linear internal format, the values in it should be
|
||||
// sRGB, not linear!
|
||||
gl.disable(glow::FRAMEBUFFER_SRGB)?;
|
||||
|
||||
// Bind the target for reading.
|
||||
let fbobind = gl.bind_framebuffer(target.framebuffer(), FramebufferTarget::Read);
|
||||
|
||||
// Blit
|
||||
let (w, h) = (target.width as i32, target.height as i32);
|
||||
gl.blit_framebuffer(
|
||||
(0, 0, w, h),
|
||||
(0, 0, w, h),
|
||||
glow::COLOR_BUFFER_BIT,
|
||||
glow::NEAREST,
|
||||
);
|
||||
|
||||
gl.unbind_framebuffer(fbobind);
|
||||
if let Err(err) = gl.enable(glow::FRAMEBUFFER_SRGB) {
|
||||
log::error!("Error re-enabling GL_FRAMEBUFFER_SRGB, err={err}");
|
||||
}
|
||||
|
||||
// Keep track of this target in case we need to repeat it.
|
||||
*last_target.lock().unwrap_or_else(PoisonError::into_inner) = Some(Arc::clone(target));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl ImageTextureFormat {
|
||||
fn to_pixel_format(self) -> PixelFormat {
|
||||
match self {
|
||||
ImageTextureFormat::Gray => PixelFormat::Red,
|
||||
ImageTextureFormat::Rgba => PixelFormat::Rgba,
|
||||
ImageTextureFormat::Bgra => PixelFormat::Bgra,
|
||||
}
|
||||
pub use crate::graphics_api::ImageTextureFormat;
|
||||
|
||||
/// The GL pixel format of an [`ImageTextureFormat`].
|
||||
fn to_pixel_format(format: ImageTextureFormat) -> PixelFormat {
|
||||
match format {
|
||||
ImageTextureFormat::Gray => PixelFormat::Red,
|
||||
ImageTextureFormat::Rgba => PixelFormat::Rgba,
|
||||
ImageTextureFormat::Bgra => PixelFormat::Bgra,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +146,7 @@ impl OpenGL {
|
||||
OpenGL {
|
||||
gl: None,
|
||||
blending,
|
||||
last_target: None,
|
||||
last_target: LastTarget::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +156,7 @@ impl OpenGL {
|
||||
OpenGL {
|
||||
gl: Some(gl),
|
||||
blending,
|
||||
last_target: None,
|
||||
last_target: LastTarget::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,42 +277,18 @@ impl OpenGL {
|
||||
}
|
||||
|
||||
/// Present the provided target.
|
||||
pub fn present(&mut self, target: &Arc<Target>) -> Result<(), Error> {
|
||||
let gl = self.gl_or_err()?.clone();
|
||||
|
||||
// In order to present a target we blit it to the default framebuffer.
|
||||
|
||||
// We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
|
||||
// values may be linearized as they're copied, but even though the draw
|
||||
// framebuffer has a linear internal format, the values in it should be
|
||||
// sRGB, not linear!
|
||||
gl.disable(glow::FRAMEBUFFER_SRGB)?;
|
||||
|
||||
// Bind the target for reading.
|
||||
let fbobind = gl.bind_framebuffer(target.framebuffer(), FramebufferTarget::Read);
|
||||
|
||||
// Blit
|
||||
let (w, h) = (target.width as i32, target.height as i32);
|
||||
gl.blit_framebuffer(
|
||||
(0, 0, w, h),
|
||||
(0, 0, w, h),
|
||||
glow::COLOR_BUFFER_BIT,
|
||||
glow::NEAREST,
|
||||
);
|
||||
|
||||
gl.unbind_framebuffer(fbobind);
|
||||
if let Err(err) = gl.enable(glow::FRAMEBUFFER_SRGB) {
|
||||
log::error!("Error re-enabling GL_FRAMEBUFFER_SRGB, err={err}");
|
||||
}
|
||||
|
||||
// Keep track of this target in case we need to repeat it.
|
||||
self.last_target = Some(Arc::clone(target));
|
||||
Ok(())
|
||||
pub fn present(&self, target: &Arc<Target>) -> Result<(), Error> {
|
||||
present_target(self.gl_or_err()?, &self.last_target, target)
|
||||
}
|
||||
|
||||
/// Present the last presented target again.
|
||||
pub fn present_last_target(&mut self) -> Result<(), Error> {
|
||||
if let Some(target) = self.last_target.clone() {
|
||||
pub fn present_last_target(&self) -> Result<(), Error> {
|
||||
let last = self
|
||||
.last_target
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.clone();
|
||||
if let Some(target) = last {
|
||||
self.present(&target)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -290,14 +296,25 @@ impl OpenGL {
|
||||
|
||||
/// The most recently presented target, if any (also set when the blit to
|
||||
/// the default framebuffer is a no-op, e.g. on a surfaceless context).
|
||||
pub fn last_target(&self) -> Option<&Arc<Target>> {
|
||||
self.last_target.as_ref()
|
||||
pub fn last_target(&self) -> Option<Arc<Target>> {
|
||||
self.last_target
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Drop the remembered last-presented target (call when the target it
|
||||
/// refers to is being replaced, e.g. on resize).
|
||||
pub fn clear_last_target(&mut self) {
|
||||
self.last_target = None;
|
||||
pub fn clear_last_target(&self) {
|
||||
*self
|
||||
.last_target
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner) = None;
|
||||
}
|
||||
|
||||
/// The slot [`Frame`]s record the target they present in.
|
||||
pub(crate) fn last_target_slot(&self) -> LastTarget {
|
||||
Arc::clone(&self.last_target)
|
||||
}
|
||||
|
||||
/// Returns the options to use when constructing buffers.
|
||||
@@ -357,7 +374,7 @@ impl OpenGL {
|
||||
srgb: bool,
|
||||
) -> texture::Options {
|
||||
texture::Options {
|
||||
format: format.to_pixel_format(),
|
||||
format: to_pixel_format(format),
|
||||
internal_format: if srgb {
|
||||
InternalFormat::Srgba
|
||||
} else {
|
||||
@@ -404,7 +421,7 @@ impl OpenGL {
|
||||
|
||||
/// Begin a frame. The target is presented via this API when the frame is
|
||||
/// completed (see [`Frame::complete`]).
|
||||
pub fn begin_frame(&mut self, target: Arc<Target>) -> Frame<'_> {
|
||||
pub fn begin_frame(&self, target: Arc<Target>) -> Frame {
|
||||
Frame::begin(frame::Options::default(), self, target)
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,14 @@
|
||||
//! Wrapper for handling render passes (`opengl/RenderPass.zig`).
|
||||
use super::{
|
||||
gl::{Attachment as GlAttachment, BufferTarget, FramebufferTarget, Gl, Primitive, RawBuffer},
|
||||
pipeline::Pipeline,
|
||||
sampler::Sampler,
|
||||
target::Target,
|
||||
texture::Texture,
|
||||
OpenGL,
|
||||
gl::{Attachment as GlAttachment, BufferTarget, FramebufferTarget, Gl},
|
||||
};
|
||||
|
||||
/// What a color attachment renders into.
|
||||
pub enum AttachmentTarget<'a> {
|
||||
Texture(&'a Texture),
|
||||
Target(&'a Target),
|
||||
}
|
||||
pub type AttachmentTarget<'a> = crate::graphics_api::AttachmentTarget<'a, OpenGL>;
|
||||
|
||||
/// Describes a color attachment.
|
||||
pub struct Attachment<'a> {
|
||||
pub target: AttachmentTarget<'a>,
|
||||
pub clear_color: Option<[f32; 4]>,
|
||||
}
|
||||
pub type Attachment<'a> = crate::graphics_api::Attachment<'a, OpenGL>;
|
||||
|
||||
/// Options for beginning a render pass.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -27,32 +18,10 @@ pub struct Options<'a> {
|
||||
}
|
||||
|
||||
/// Describes the draw call for a step.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Draw {
|
||||
pub ty: Primitive,
|
||||
pub vertex_count: usize,
|
||||
pub instance_count: usize,
|
||||
}
|
||||
|
||||
impl Draw {
|
||||
pub fn new(ty: Primitive, vertex_count: usize) -> Self {
|
||||
Draw {
|
||||
ty,
|
||||
vertex_count,
|
||||
instance_count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use crate::graphics_api::Draw;
|
||||
|
||||
/// Describes a step in a render pass.
|
||||
pub struct Step<'a> {
|
||||
pub pipeline: &'a Pipeline,
|
||||
pub uniforms: Option<RawBuffer>,
|
||||
pub buffers: &'a [Option<RawBuffer>],
|
||||
pub textures: &'a [Option<&'a Texture>],
|
||||
pub samplers: &'a [Option<&'a Sampler>],
|
||||
pub draw: Draw,
|
||||
}
|
||||
pub type Step<'a> = crate::graphics_api::Step<'a, OpenGL>;
|
||||
|
||||
pub struct RenderPass<'a> {
|
||||
gl: Gl,
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
//! Shader pipelines and GPU data layouts for OpenGL (`opengl/shaders.zig`).
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
use super::{
|
||||
gl::{self, AttribType, Gl},
|
||||
pipeline::{Attribute, Options as PipelineOptions, Pipeline, StepFunction, Vertex},
|
||||
};
|
||||
use crate::math::Mat;
|
||||
|
||||
/// `shaders/glsl/common.glsl`, the only `#include`d file.
|
||||
const COMMON_GLSL: &str = include_str!("../../shaders/glsl/common.glsl");
|
||||
@@ -160,167 +157,19 @@ pub fn process_includes(contents: &str) -> String {
|
||||
contents.to_owned()
|
||||
}
|
||||
|
||||
/// The uniforms that are passed to our shaders (std140 layout, 144 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct Uniforms {
|
||||
/// The projection matrix for turning world coordinates to normalized.
|
||||
/// This is calculated based on the size of the screen.
|
||||
pub projection_matrix: Mat,
|
||||
|
||||
/// Size of the screen (render target) in pixels.
|
||||
pub screen_size: [f32; 2],
|
||||
|
||||
/// Size of a single cell in pixels, unscaled.
|
||||
pub cell_size: [f32; 2],
|
||||
|
||||
/// Size of the grid in columns and rows.
|
||||
pub grid_size: [u16; 2],
|
||||
|
||||
// Explicit padding so that `grid_padding` is 16-byte aligned (std140).
|
||||
pub pad0: [u8; 12],
|
||||
|
||||
/// The padding around the terminal grid in pixels. In order:
|
||||
/// top, right, bottom, left.
|
||||
pub grid_padding: [f32; 4],
|
||||
|
||||
/// Bit mask defining which directions to
|
||||
/// extend cell colors in to the padding.
|
||||
/// Order, LSB first: left, right, up, down. See [`PaddingExtend`].
|
||||
pub padding_extend: u32,
|
||||
|
||||
/// The minimum contrast ratio for text. The contrast ratio is calculated
|
||||
/// according to the WCAG 2.0 spec.
|
||||
pub min_contrast: f32,
|
||||
|
||||
/// The cursor position and color.
|
||||
pub cursor_pos: [u16; 2],
|
||||
pub cursor_color: [u8; 4],
|
||||
|
||||
/// The background color for the whole surface.
|
||||
pub bg_color: [u8; 4],
|
||||
|
||||
/// Various booleans, in a packed bitfield for space efficiency. See [`Bools`].
|
||||
pub bools: u32,
|
||||
|
||||
// Tail padding to the struct's 16-byte alignment (extern struct size).
|
||||
pub pad1: [u8; 8],
|
||||
}
|
||||
|
||||
/// The `bools` bitfield of [`Uniforms`] (a `packed struct(u32)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Bools {
|
||||
/// Whether the cursor is 2 cells wide.
|
||||
pub cursor_wide: bool,
|
||||
|
||||
/// Indicates that colors provided to the shader are already in
|
||||
/// the P3 color space, so they don't need to be converted from
|
||||
/// sRGB.
|
||||
pub use_display_p3: bool,
|
||||
|
||||
/// Indicates that the color attachments for the shaders have
|
||||
/// an `*_srgb` pixel format, which means the shaders need to
|
||||
/// output linear RGB colors rather than gamma encoded colors,
|
||||
/// since blending will be performed in linear space and then
|
||||
/// the GPU itself will re-encode the colors for storage.
|
||||
pub use_linear_blending: bool,
|
||||
|
||||
/// Enables a weight correction step that makes text rendered
|
||||
/// with linear alpha blending have a similar apparent weight
|
||||
/// (thickness) to gamma-incorrect blending.
|
||||
pub use_linear_correction: bool,
|
||||
}
|
||||
|
||||
impl Bools {
|
||||
pub const CURSOR_WIDE: u32 = 1;
|
||||
pub const USE_DISPLAY_P3: u32 = 2;
|
||||
pub const USE_LINEAR_BLENDING: u32 = 4;
|
||||
pub const USE_LINEAR_CORRECTION: u32 = 8;
|
||||
|
||||
pub fn to_bits(self) -> u32 {
|
||||
u32::from(self.cursor_wide)
|
||||
| u32::from(self.use_display_p3) << 1
|
||||
| u32::from(self.use_linear_blending) << 2
|
||||
| u32::from(self.use_linear_correction) << 3
|
||||
}
|
||||
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
Bools {
|
||||
cursor_wide: bits & Self::CURSOR_WIDE != 0,
|
||||
use_display_p3: bits & Self::USE_DISPLAY_P3 != 0,
|
||||
use_linear_blending: bits & Self::USE_LINEAR_BLENDING != 0,
|
||||
use_linear_correction: bits & Self::USE_LINEAR_CORRECTION != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `padding_extend` bitfield of [`Uniforms`] (a `packed struct(u32)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct PaddingExtend {
|
||||
pub left: bool,
|
||||
pub right: bool,
|
||||
pub up: bool,
|
||||
pub down: bool,
|
||||
}
|
||||
|
||||
impl PaddingExtend {
|
||||
pub fn to_bits(self) -> u32 {
|
||||
u32::from(self.left)
|
||||
| u32::from(self.right) << 1
|
||||
| u32::from(self.up) << 2
|
||||
| u32::from(self.down) << 3
|
||||
}
|
||||
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
PaddingExtend {
|
||||
left: bits & 1 != 0,
|
||||
right: bits & 2 != 0,
|
||||
up: bits & 4 != 0,
|
||||
down: bits & 8 != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which atlas a glyph lives in (`CellText.Atlas`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CellTextAtlas {
|
||||
Grayscale = 0,
|
||||
Color = 1,
|
||||
}
|
||||
|
||||
/// This is a single parameter for the terminal cell shader (32 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Pod, Zeroable)]
|
||||
pub struct CellText {
|
||||
pub glyph_pos: [u32; 2],
|
||||
pub glyph_size: [u32; 2],
|
||||
pub bearings: [i16; 2],
|
||||
pub grid_pos: [u16; 2],
|
||||
pub color: [u8; 4],
|
||||
/// A [`CellTextAtlas`] value.
|
||||
pub atlas: u8,
|
||||
/// Bit 0: `no_min_contrast`, bit 1: `is_cursor_glyph`. See the consts below.
|
||||
pub bools: u8,
|
||||
pub pad: [u8; 2],
|
||||
}
|
||||
|
||||
impl CellText {
|
||||
pub const NO_MIN_CONTRAST: u8 = 1;
|
||||
pub const IS_CURSOR_GLYPH: u8 = 2;
|
||||
|
||||
pub fn atlas(&self) -> CellTextAtlas {
|
||||
if self.atlas == 0 {
|
||||
CellTextAtlas::Grayscale
|
||||
} else {
|
||||
CellTextAtlas::Color
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_atlas(&mut self, a: CellTextAtlas) {
|
||||
self.atlas = a as u8;
|
||||
}
|
||||
}
|
||||
pub use crate::shader_types::{
|
||||
BgImage,
|
||||
BgImageFit,
|
||||
BgImageInfo,
|
||||
BgImagePosition,
|
||||
Bools,
|
||||
CellBg,
|
||||
CellText,
|
||||
CellTextAtlas,
|
||||
Image,
|
||||
PaddingExtend,
|
||||
Uniforms,
|
||||
};
|
||||
|
||||
impl Vertex for CellText {
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
@@ -362,21 +211,6 @@ impl Vertex for CellText {
|
||||
];
|
||||
}
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub type CellBg = [u8; 4];
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
/// (48 bytes: the Zig extern struct is 16-byte aligned.)
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct Image {
|
||||
pub grid_pos: [f32; 2],
|
||||
pub cell_offset: [f32; 2],
|
||||
pub source_rect: [f32; 4],
|
||||
pub dest_size: [f32; 2],
|
||||
pub pad: [f32; 2],
|
||||
}
|
||||
|
||||
impl Vertex for Image {
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute {
|
||||
@@ -402,53 +236,6 @@ impl Vertex for Image {
|
||||
];
|
||||
}
|
||||
|
||||
/// Single parameter for the bg image shader (8 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct BgImage {
|
||||
pub opacity: f32,
|
||||
/// A packed [`BgImageInfo`] byte.
|
||||
pub info: u8,
|
||||
pub pad: [u8; 3],
|
||||
}
|
||||
|
||||
/// `BgImage.Info`, a `packed struct(u8)`: position:u4, fit:u2, repeat:u1, pad:u1.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BgImageInfo {
|
||||
pub position: BgImagePosition,
|
||||
pub fit: BgImageFit,
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
impl BgImageInfo {
|
||||
pub fn to_bits(self) -> u8 {
|
||||
(self.position as u8) | (self.fit as u8) << 4 | u8::from(self.repeat) << 6
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum BgImagePosition {
|
||||
Tl = 0,
|
||||
Tc = 1,
|
||||
Tr = 2,
|
||||
Ml = 3,
|
||||
Mc = 4,
|
||||
Mr = 5,
|
||||
Bl = 6,
|
||||
Bc = 7,
|
||||
Br = 8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum BgImageFit {
|
||||
Contain = 0,
|
||||
Cover = 1,
|
||||
Stretch = 2,
|
||||
None = 3,
|
||||
}
|
||||
|
||||
impl Vertex for BgImage {
|
||||
const ATTRIBUTES: &'static [Attribute] = &[
|
||||
Attribute {
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
//! GPU data layouts shared by every graphics API (`opengl/shaders.zig` and
|
||||
//! `metal/shaders.zig` define identical CPU-side structs; the only difference
|
||||
//! is the Metal `Uniforms.bools` layout, see `crate::metal::shaders`).
|
||||
//!
|
||||
//! The CPU-side [`Uniforms`] is the std140 layout used by OpenGL; APIs with a
|
||||
//! different GPU layout convert it with `GraphicsApi::gpu_uniforms`.
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
use crate::math::Mat;
|
||||
|
||||
/// The uniforms that are passed to our shaders (std140 layout, 144 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct Uniforms {
|
||||
/// The projection matrix for turning world coordinates to normalized.
|
||||
/// This is calculated based on the size of the screen.
|
||||
pub projection_matrix: Mat,
|
||||
|
||||
/// Size of the screen (render target) in pixels.
|
||||
pub screen_size: [f32; 2],
|
||||
|
||||
/// Size of a single cell in pixels, unscaled.
|
||||
pub cell_size: [f32; 2],
|
||||
|
||||
/// Size of the grid in columns and rows.
|
||||
pub grid_size: [u16; 2],
|
||||
|
||||
// Explicit padding so that `grid_padding` is 16-byte aligned (std140).
|
||||
pub pad0: [u8; 12],
|
||||
|
||||
/// The padding around the terminal grid in pixels. In order:
|
||||
/// top, right, bottom, left.
|
||||
pub grid_padding: [f32; 4],
|
||||
|
||||
/// Bit mask defining which directions to
|
||||
/// extend cell colors in to the padding.
|
||||
/// Order, LSB first: left, right, up, down. See [`PaddingExtend`].
|
||||
pub padding_extend: u32,
|
||||
|
||||
/// The minimum contrast ratio for text. The contrast ratio is calculated
|
||||
/// according to the WCAG 2.0 spec.
|
||||
pub min_contrast: f32,
|
||||
|
||||
/// The cursor position and color.
|
||||
pub cursor_pos: [u16; 2],
|
||||
pub cursor_color: [u8; 4],
|
||||
|
||||
/// The background color for the whole surface.
|
||||
pub bg_color: [u8; 4],
|
||||
|
||||
/// Various booleans, in a packed bitfield for space efficiency. See [`Bools`].
|
||||
pub bools: u32,
|
||||
|
||||
// Tail padding to the struct's 16-byte alignment (extern struct size).
|
||||
pub pad1: [u8; 8],
|
||||
}
|
||||
|
||||
/// The `bools` bitfield of [`Uniforms`] (a `packed struct(u32)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Bools {
|
||||
/// Whether the cursor is 2 cells wide.
|
||||
pub cursor_wide: bool,
|
||||
|
||||
/// Indicates that colors provided to the shader are already in
|
||||
/// the P3 color space, so they don't need to be converted from
|
||||
/// sRGB.
|
||||
pub use_display_p3: bool,
|
||||
|
||||
/// Indicates that the color attachments for the shaders have
|
||||
/// an `*_srgb` pixel format, which means the shaders need to
|
||||
/// output linear RGB colors rather than gamma encoded colors,
|
||||
/// since blending will be performed in linear space and then
|
||||
/// the GPU itself will re-encode the colors for storage.
|
||||
pub use_linear_blending: bool,
|
||||
|
||||
/// Enables a weight correction step that makes text rendered
|
||||
/// with linear alpha blending have a similar apparent weight
|
||||
/// (thickness) to gamma-incorrect blending.
|
||||
pub use_linear_correction: bool,
|
||||
}
|
||||
|
||||
impl Bools {
|
||||
pub const CURSOR_WIDE: u32 = 1;
|
||||
pub const USE_DISPLAY_P3: u32 = 2;
|
||||
pub const USE_LINEAR_BLENDING: u32 = 4;
|
||||
pub const USE_LINEAR_CORRECTION: u32 = 8;
|
||||
|
||||
pub fn to_bits(self) -> u32 {
|
||||
u32::from(self.cursor_wide)
|
||||
| u32::from(self.use_display_p3) << 1
|
||||
| u32::from(self.use_linear_blending) << 2
|
||||
| u32::from(self.use_linear_correction) << 3
|
||||
}
|
||||
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
Bools {
|
||||
cursor_wide: bits & Self::CURSOR_WIDE != 0,
|
||||
use_display_p3: bits & Self::USE_DISPLAY_P3 != 0,
|
||||
use_linear_blending: bits & Self::USE_LINEAR_BLENDING != 0,
|
||||
use_linear_correction: bits & Self::USE_LINEAR_CORRECTION != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `padding_extend` bitfield of [`Uniforms`] (a `packed struct(u32)` upstream).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct PaddingExtend {
|
||||
pub left: bool,
|
||||
pub right: bool,
|
||||
pub up: bool,
|
||||
pub down: bool,
|
||||
}
|
||||
|
||||
impl PaddingExtend {
|
||||
pub fn to_bits(self) -> u32 {
|
||||
u32::from(self.left)
|
||||
| u32::from(self.right) << 1
|
||||
| u32::from(self.up) << 2
|
||||
| u32::from(self.down) << 3
|
||||
}
|
||||
|
||||
pub fn from_bits(bits: u32) -> Self {
|
||||
PaddingExtend {
|
||||
left: bits & 1 != 0,
|
||||
right: bits & 2 != 0,
|
||||
up: bits & 4 != 0,
|
||||
down: bits & 8 != 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which atlas a glyph lives in (`CellText.Atlas`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CellTextAtlas {
|
||||
Grayscale = 0,
|
||||
Color = 1,
|
||||
}
|
||||
|
||||
/// This is a single parameter for the terminal cell shader (32 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Pod, Zeroable)]
|
||||
pub struct CellText {
|
||||
pub glyph_pos: [u32; 2],
|
||||
pub glyph_size: [u32; 2],
|
||||
pub bearings: [i16; 2],
|
||||
pub grid_pos: [u16; 2],
|
||||
pub color: [u8; 4],
|
||||
/// A [`CellTextAtlas`] value.
|
||||
pub atlas: u8,
|
||||
/// Bit 0: `no_min_contrast`, bit 1: `is_cursor_glyph`. See the consts below.
|
||||
pub bools: u8,
|
||||
pub pad: [u8; 2],
|
||||
}
|
||||
|
||||
impl CellText {
|
||||
pub const NO_MIN_CONTRAST: u8 = 1;
|
||||
pub const IS_CURSOR_GLYPH: u8 = 2;
|
||||
|
||||
pub fn atlas(&self) -> CellTextAtlas {
|
||||
if self.atlas == 0 {
|
||||
CellTextAtlas::Grayscale
|
||||
} else {
|
||||
CellTextAtlas::Color
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_atlas(&mut self, a: CellTextAtlas) {
|
||||
self.atlas = a as u8;
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a single parameter for the cell bg shader.
|
||||
pub type CellBg = [u8; 4];
|
||||
|
||||
/// Single parameter for the image shader. See shader for field details.
|
||||
/// (48 bytes: the Zig extern struct is 16-byte aligned.)
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct Image {
|
||||
pub grid_pos: [f32; 2],
|
||||
pub cell_offset: [f32; 2],
|
||||
pub source_rect: [f32; 4],
|
||||
pub dest_size: [f32; 2],
|
||||
pub pad: [f32; 2],
|
||||
}
|
||||
|
||||
/// Single parameter for the bg image shader (8 bytes).
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Pod, Zeroable)]
|
||||
pub struct BgImage {
|
||||
pub opacity: f32,
|
||||
/// A packed [`BgImageInfo`] byte.
|
||||
pub info: u8,
|
||||
pub pad: [u8; 3],
|
||||
}
|
||||
|
||||
/// `BgImage.Info`, a `packed struct(u8)`: position:u4, fit:u2, repeat:u1, pad:u1.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BgImageInfo {
|
||||
pub position: BgImagePosition,
|
||||
pub fit: BgImageFit,
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
impl BgImageInfo {
|
||||
pub fn to_bits(self) -> u8 {
|
||||
(self.position as u8) | (self.fit as u8) << 4 | u8::from(self.repeat) << 6
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum BgImagePosition {
|
||||
Tl = 0,
|
||||
Tc = 1,
|
||||
Tr = 2,
|
||||
Ml = 3,
|
||||
Mc = 4,
|
||||
Mr = 5,
|
||||
Bl = 6,
|
||||
Bc = 7,
|
||||
Br = 8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum BgImageFit {
|
||||
Contain = 0,
|
||||
Cover = 1,
|
||||
Stretch = 2,
|
||||
None = 3,
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use std::{io::Read, path::Path};
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use naga::{
|
||||
back::glsl as glsl_out,
|
||||
back::{glsl as glsl_out, msl as msl_out},
|
||||
front::glsl as glsl_in,
|
||||
valid::{Capabilities, ModuleInfo, ValidationFlags, Validator},
|
||||
};
|
||||
@@ -71,8 +71,7 @@ impl Default for Uniforms {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
Glsl,
|
||||
/// Metal is out of scope for the Rust port; converting to it fails with
|
||||
/// [`Error::Unsupported`].
|
||||
/// Metal Shading Language (naga's `msl-out` replaces spirv-cross).
|
||||
Msl,
|
||||
}
|
||||
|
||||
@@ -101,7 +100,7 @@ pub enum Error {
|
||||
#[error("shader validation failed")]
|
||||
ValidationFailed,
|
||||
/// Writing the target GLSL failed (upstream: `error.SpvcFailed`).
|
||||
#[error("GLSL generation failed: {0}")]
|
||||
#[error("shader generation failed: {0}")]
|
||||
SpvcFailed(String),
|
||||
#[error("unsupported shader target")]
|
||||
Unsupported,
|
||||
@@ -171,7 +170,7 @@ pub fn load_from_file(path: &Path, target: Target) -> Result<String, Error> {
|
||||
|
||||
match target {
|
||||
Target::Glsl => glsl_from_compiled(&compiled),
|
||||
Target::Msl => Err(Error::Unsupported),
|
||||
Target::Msl => msl_from_compiled(&compiled),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +294,86 @@ fn adapt_for_naga(full: &str) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
/// The name of the fragment entry point in generated MSL. spirv-cross calls
|
||||
/// it `main0` because `main` is reserved in MSL; naga would turn `main0` into
|
||||
/// `main0_` (it suffixes names ending in a digit), so we use a name it keeps
|
||||
/// as is. The Metal renderer looks the function up by this name.
|
||||
pub const MSL_ENTRY_POINT: &str = "ghostty_main";
|
||||
|
||||
/// Convert a compiled shader to MSL (`mslFromSpv`).
|
||||
///
|
||||
/// The resource slots follow what spirv-cross produces with
|
||||
/// `SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING` for the shadertoy
|
||||
/// prefix: the `Globals` uniform block at `[[buffer(1)]]` ("we need this to
|
||||
/// properly locate the uniform block to index 1") and `iChannel0` as
|
||||
/// `[[texture(0)]]` / `[[sampler(0)]]`, which is where the Metal render pass
|
||||
/// binds them.
|
||||
pub fn msl_from_compiled(compiled: &Compiled) -> Result<String, Error> {
|
||||
let fail = |e: &dyn std::fmt::Display| {
|
||||
log::warn!("spirv-cross error message={e}");
|
||||
Error::SpvcFailed(e.to_string())
|
||||
};
|
||||
|
||||
// Rename the entry point (info is indexed by entry point order, which
|
||||
// doesn't change).
|
||||
let mut module = compiled.module.clone();
|
||||
for ep in &mut module.entry_points {
|
||||
if ep.name == "main" {
|
||||
ep.name = MSL_ENTRY_POINT.into();
|
||||
}
|
||||
}
|
||||
|
||||
let mut resources = msl_out::EntryPointResources::default();
|
||||
let mut bind = |binding: u32, target: msl_out::BindTarget| {
|
||||
resources
|
||||
.resources
|
||||
.insert(naga::ResourceBinding { group: 0, binding }, target);
|
||||
};
|
||||
// iChannel0 texture (the sampler is split off at binding 2, see
|
||||
// `adapt_for_naga`).
|
||||
bind(
|
||||
0,
|
||||
msl_out::BindTarget {
|
||||
texture: Some(0),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
bind(
|
||||
1,
|
||||
msl_out::BindTarget {
|
||||
buffer: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
bind(
|
||||
2,
|
||||
msl_out::BindTarget {
|
||||
sampler: Some(msl_out::BindSamplerTarget::Resource(0)),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut options = msl_out::Options {
|
||||
// Needed for `[[position]]` fragment inputs etc. on all supported
|
||||
// macOS versions (the metallib is built for macOS 10.14 / MSL 2.1).
|
||||
lang_version: (2, 1),
|
||||
fake_missing_bindings: false,
|
||||
..Default::default()
|
||||
};
|
||||
options
|
||||
.per_entry_point_map
|
||||
.insert(MSL_ENTRY_POINT.into(), resources);
|
||||
|
||||
let pipeline = msl_out::PipelineOptions {
|
||||
entry_point: Some((naga::ShaderStage::Fragment, MSL_ENTRY_POINT.into())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (msl, _info) = msl_out::write_string(&module, &compiled.info, &options, &pipeline)
|
||||
.map_err(|e| fail(&e))?;
|
||||
Ok(msl)
|
||||
}
|
||||
|
||||
/// Convert a compiled shader to GLSL 430 core (`glslFromSpv`).
|
||||
pub fn glsl_from_compiled(compiled: &Compiled) -> Result<String, Error> {
|
||||
const GLSL_VERSION: u16 = 430;
|
||||
@@ -383,17 +462,38 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadertoy_to_msl_unsupported() {
|
||||
fn shadertoy_to_msl() {
|
||||
// `shadertoy to msl`
|
||||
let compiled = compile_shadertoy(TEST_CRT, None).unwrap();
|
||||
assert_eq!(compiled.module.entry_points.len(), 1);
|
||||
let msl = msl_from_compiled(&compiled).unwrap();
|
||||
assert!(msl.contains("#include <metal_stdlib>"));
|
||||
// The Metal renderer looks the fragment function up by this name.
|
||||
assert!(
|
||||
msl.contains("fragment ghostty_mainOutput ghostty_main("),
|
||||
"{msl}"
|
||||
);
|
||||
// Uniform block at buffer 1, channel at texture 0 / sampler 0.
|
||||
assert!(msl.contains("[[buffer(1)]]"), "{msl}");
|
||||
assert!(msl.contains("[[texture(0)]]"), "{msl}");
|
||||
assert!(msl.contains("[[sampler(0)]]"), "{msl}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadertoy_focus_to_msl() {
|
||||
let compiled = compile_shadertoy(TEST_FOCUS, None).unwrap();
|
||||
let msl = msl_from_compiled(&compiled).unwrap();
|
||||
assert!(msl.contains("ghostty_main("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_file_msl() {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-shadertoy-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("crt.glsl");
|
||||
std::fs::write(&path, TEST_CRT).unwrap();
|
||||
assert!(matches!(
|
||||
load_from_file(&path, Target::Msl),
|
||||
Err(Error::Unsupported)
|
||||
));
|
||||
let msl = load_from_file(&path, Target::Msl).unwrap();
|
||||
assert!(msl.contains("ghostty_main("));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,9 @@ use ghostty_config::{config::Config, types_a::CustomShaderAnimation};
|
||||
use ghostty_datastruct::blocking_queue::{BlockingQueue, Timeout};
|
||||
|
||||
use crate::{
|
||||
Api,
|
||||
generic::{Host, Renderer},
|
||||
graphics_api::GraphicsApi,
|
||||
message::Message,
|
||||
state::SharedState,
|
||||
};
|
||||
@@ -161,12 +163,72 @@ impl Drop for Running {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `Renderer::loop_exit` when the thread leaves its main loop.
|
||||
struct LoopGuard<G: GraphicsApi>(Arc<Renderer<G>>);
|
||||
|
||||
impl<G: GraphicsApi> Drop for LoopGuard<G> {
|
||||
fn drop(&mut self) {
|
||||
self.0.loop_exit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `Renderer::thread_exit` when the thread exits.
|
||||
struct ThreadGuard<G: GraphicsApi>(Arc<Renderer<G>>);
|
||||
|
||||
impl<G: GraphicsApi> Drop for ThreadGuard<G> {
|
||||
fn drop(&mut self) {
|
||||
self.0.thread_exit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `f` inside an autorelease pool on Darwin ("This is effectively an
|
||||
/// @autoreleasepool{} block, which we need in order to ensure that
|
||||
/// autoreleased objects are properly released."); just calls `f` elsewhere.
|
||||
fn with_autorelease_pool<R>(f: impl FnOnce() -> R) -> R {
|
||||
#[cfg(target_vendor = "apple")]
|
||||
{
|
||||
objc2::rc::autoreleasepool(|_| f())
|
||||
}
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
{
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
/// The QoS class for the thread's visibility / focus state
|
||||
/// (`setQosClass`): "If we aren't visible (our view is fully occluded) then we
|
||||
/// always drop our rendering priority down because it's just mostly wasted
|
||||
/// work. If we're not focused, but we're visible, then we set a higher than
|
||||
/// default priority because framerates still matter but it isn't as important
|
||||
/// as when we're focused. We are focused and visible, we are the definition of
|
||||
/// user interactive."
|
||||
#[cfg(any(target_vendor = "apple", test))]
|
||||
fn qos_for(visible: bool, focused: bool) -> QosClass {
|
||||
if !visible {
|
||||
QosClass::Utility
|
||||
} else if !focused {
|
||||
QosClass::UserInitiated
|
||||
} else {
|
||||
QosClass::UserInteractive
|
||||
}
|
||||
}
|
||||
|
||||
/// `ghostty_os::macos::QosClass` (re-declared so the policy can be tested on
|
||||
/// every platform).
|
||||
#[cfg(any(target_vendor = "apple", test))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum QosClass {
|
||||
UserInteractive,
|
||||
UserInitiated,
|
||||
Utility,
|
||||
}
|
||||
|
||||
/// The renderer thread state. Construct with [`Thread::new`] then start it
|
||||
/// with [`Thread::spawn`] (or run [`Thread::thread_main`] on a thread you
|
||||
/// manage).
|
||||
pub struct Thread {
|
||||
pub struct Thread<G: GraphicsApi = Api> {
|
||||
/// The underlying renderer implementation.
|
||||
renderer: Arc<Renderer>,
|
||||
renderer: Arc<Renderer<G>>,
|
||||
|
||||
/// Pointer to the shared state that is used to generate the final render.
|
||||
state: SharedState,
|
||||
@@ -177,13 +239,16 @@ pub struct Thread {
|
||||
/// Configuration we need derived from the main config.
|
||||
config: DerivedConfig,
|
||||
|
||||
/// If true (the default, GTK), draws are requested from the apprt via
|
||||
/// If true (the default, except on Apple platforms), draws are requested from the apprt via
|
||||
/// [`Host::redraw_surface`] instead of being performed on this thread
|
||||
/// (`must_draw_from_app_thread`).
|
||||
pub draw_on_app_thread: bool,
|
||||
|
||||
flags: Flags,
|
||||
|
||||
/// The surface dimensions recorded in crash reports.
|
||||
pub crash_dims: Option<Arc<ghostty_crash::SurfaceDims>>,
|
||||
|
||||
handle: Handle,
|
||||
wakeup_rx: Receiver<()>,
|
||||
wakeup_tx: Sender<()>,
|
||||
@@ -198,16 +263,16 @@ pub struct Thread {
|
||||
draw_deadline: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
impl<G: GraphicsApi> Thread<G> {
|
||||
/// Initialize the thread. This does not START the thread. This only sets
|
||||
/// up all the internal state necessary prior to starting the thread. It
|
||||
/// is up to the caller to start the thread with [`Thread::spawn`].
|
||||
pub fn new(
|
||||
config: &Config,
|
||||
renderer: Arc<Renderer>,
|
||||
renderer: Arc<Renderer<G>>,
|
||||
state: SharedState,
|
||||
host: Arc<dyn Host>,
|
||||
) -> Thread {
|
||||
) -> Thread<G> {
|
||||
// Wakeups, stop and draw-now coalesce (xev.Async semantics).
|
||||
let (wakeup_tx, wakeup_rx) = bounded(1);
|
||||
let (stop_tx, stop_rx) = bounded(1);
|
||||
@@ -225,13 +290,17 @@ impl Thread {
|
||||
state,
|
||||
host,
|
||||
config: DerivedConfig::new(config),
|
||||
draw_on_app_thread: true,
|
||||
// Only the GTK apprt must draw from the app thread
|
||||
// (`apprt.App.must_draw_from_app_thread`); the embedded macOS /
|
||||
// iOS apprt lets the renderer thread draw (Metal is thread-safe).
|
||||
draw_on_app_thread: !cfg!(target_vendor = "apple"),
|
||||
flags: Flags {
|
||||
cursor_blink_visible: false,
|
||||
has_inspector: false,
|
||||
visible: true,
|
||||
focused: true,
|
||||
},
|
||||
crash_dims: None,
|
||||
handle,
|
||||
wakeup_rx,
|
||||
wakeup_tx,
|
||||
@@ -266,8 +335,34 @@ impl Thread {
|
||||
pub fn thread_main(mut self) {
|
||||
log::debug!("starting renderer thread");
|
||||
|
||||
// Run our thread start/end callbacks. For OpenGL on GTK these are
|
||||
// Right now, on Darwin, `std.Thread.setName` can only name the current
|
||||
// thread, and we have no way to get the current thread from within it,
|
||||
// so instead we use this code to name the thread instead.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
ghostty_os::macos::pthread_setname_np(c"renderer");
|
||||
|
||||
// Setup our crash metadata
|
||||
let _crash = ghostty_crash::ThreadGuard::new(
|
||||
ghostty_crash::ThreadType::Renderer,
|
||||
self.crash_dims.clone(),
|
||||
);
|
||||
|
||||
// Setup our thread QoS
|
||||
self.set_qos_class();
|
||||
|
||||
// Run our loop start/end callbacks if the renderer cares (Metal's
|
||||
// display callback, the macOS display link).
|
||||
let draw_now = self.handle.draw_now.clone();
|
||||
self.renderer.loop_enter(Arc::new(move || {
|
||||
let _ = draw_now.try_send(());
|
||||
}));
|
||||
let _loop_guard = LoopGuard(Arc::clone(&self.renderer));
|
||||
|
||||
// Run our thread start/end callbacks. This is important because some
|
||||
// renderers have to do per-thread setup. For OpenGL on GTK these are
|
||||
// no-ops: all GL work is on the main thread.
|
||||
self.renderer.thread_enter();
|
||||
let _thread_guard = ThreadGuard(Arc::clone(&self.renderer));
|
||||
|
||||
// Send an initial wakeup message so that we render right away.
|
||||
let _ = self.wakeup_tx.try_send(());
|
||||
@@ -344,6 +439,31 @@ impl Thread {
|
||||
|
||||
/// Drain the mailbox.
|
||||
fn drain_mailbox(&mut self) {
|
||||
with_autorelease_pool(|| self.drain_mailbox_inner());
|
||||
}
|
||||
|
||||
/// Set the QoS class of this thread. Thread QoS classes are only
|
||||
/// relevant on macOS.
|
||||
#[cfg_attr(not(target_vendor = "apple"), allow(clippy::unused_self))]
|
||||
fn set_qos_class(&self) {
|
||||
#[cfg(target_vendor = "apple")]
|
||||
{
|
||||
use ghostty_os::macos::{self, QosClass as OsQosClass};
|
||||
|
||||
let class = qos_for(self.flags.visible, self.flags.focused);
|
||||
let os_class = match class {
|
||||
QosClass::UserInteractive => OsQosClass::UserInteractive,
|
||||
QosClass::UserInitiated => OsQosClass::UserInitiated,
|
||||
QosClass::Utility => OsQosClass::Utility,
|
||||
};
|
||||
match macos::set_qos_class(os_class) {
|
||||
Ok(()) => log::debug!("thread QoS class set class={class:?}"),
|
||||
Err(err) => log::warn!("error setting QoS class err={err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_mailbox_inner(&mut self) {
|
||||
while let Some(message) = self.handle.mailbox.pop() {
|
||||
match message {
|
||||
Message::Crash => panic!("crash request, crashing intentionally"),
|
||||
@@ -357,6 +477,9 @@ impl Thread {
|
||||
// Set our visible state
|
||||
self.flags.visible = v;
|
||||
|
||||
// Visibility affects our QoS class
|
||||
self.set_qos_class();
|
||||
|
||||
// If we became visible then we immediately trigger a draw.
|
||||
// We don't need to update frame data because that should
|
||||
// still be happening.
|
||||
@@ -384,6 +507,9 @@ impl Thread {
|
||||
// Set our state
|
||||
self.flags.focused = v;
|
||||
|
||||
// Focus affects our QoS class
|
||||
self.set_qos_class();
|
||||
|
||||
// Set it on the renderer
|
||||
self.renderer.set_focus(v);
|
||||
|
||||
@@ -441,7 +567,7 @@ impl Thread {
|
||||
self.flags.has_inspector = v;
|
||||
}
|
||||
|
||||
Message::MacosDisplayId(_) => {}
|
||||
Message::MacosDisplayId(id) => self.renderer.set_macos_display_id(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,6 +672,14 @@ mod tests {
|
||||
assert!(!should_draw_timer_run(true, False, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qos_class_policy() {
|
||||
assert_eq!(qos_for(false, true), QosClass::Utility);
|
||||
assert_eq!(qos_for(false, false), QosClass::Utility);
|
||||
assert_eq!(qos_for(true, false), QosClass::UserInitiated);
|
||||
assert_eq!(qos_for(true, true), QosClass::UserInteractive);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intervals_match_zig() {
|
||||
assert_eq!(DRAW_INTERVAL, Duration::from_millis(8));
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! Skipped (returns early with a message) when no EGL display / GL 4.3 core
|
||||
//! context can be created, e.g. on CI without Mesa.
|
||||
|
||||
// These tests drive the OpenGL backend (Mesa); Apple platforms render with Metal.
|
||||
#![cfg(not(target_vendor = "apple"))]
|
||||
// The EGL loader calls are `unsafe fn`s in khronos-egl (test-only usage).
|
||||
#![allow(unsafe_code, clippy::unwrap_used)]
|
||||
|
||||
@@ -128,7 +130,7 @@ fn px(buf: &[u8], w: usize, x: usize, y: usize) -> [u8; 4] {
|
||||
|
||||
#[test]
|
||||
fn headless_shaders_compile_and_render() {
|
||||
let Some((_h, mut api)) = setup() else {
|
||||
let Some((_h, api)) = setup() else {
|
||||
return;
|
||||
};
|
||||
let gl: Gl = api.gl().unwrap().clone();
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
//! The generic renderer over a mock graphics API: no GPU, no GL context.
|
||||
//!
|
||||
//! The mock completes frames on a worker thread (like Metal's command buffer
|
||||
//! completion handlers) with a swap chain of three frames, which exercises the
|
||||
//! swap chain hand-back, health reporting and the step/slot conventions the
|
||||
//! real backends rely on.
|
||||
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
Mutex,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bytemuck::Pod;
|
||||
use ghostty_config::{config::Config, types_b::AlphaBlending};
|
||||
use ghostty_font::{
|
||||
Atlas,
|
||||
face::DesiredSize,
|
||||
shared_grid_set::{DerivedConfig as FontDerivedConfig, SharedGridSet},
|
||||
};
|
||||
use ghostty_renderer::{
|
||||
Health,
|
||||
generic::{DerivedConfig, Host, Renderer},
|
||||
graphics_api::{
|
||||
ApiError,
|
||||
Attachment,
|
||||
FrameCompletion,
|
||||
GpuBuffer,
|
||||
GpuFrame,
|
||||
GpuRenderPass,
|
||||
GpuShaders,
|
||||
GpuTarget,
|
||||
GpuTexture,
|
||||
GraphicsApi,
|
||||
Step,
|
||||
TextureKind,
|
||||
},
|
||||
options::Options,
|
||||
shader_types::Uniforms,
|
||||
shadertoy,
|
||||
size::{CellSize, Padding, ScreenSize, Size},
|
||||
state::State,
|
||||
};
|
||||
use ghostty_terminal::{
|
||||
page_list::Scrollbar,
|
||||
terminal::{Options as TermOptions, Terminal},
|
||||
};
|
||||
|
||||
/// What the mock saw: one entry per render pass step, as
|
||||
/// `pipeline:instances`.
|
||||
type Log = Arc<Mutex<Vec<String>>>;
|
||||
|
||||
struct MockApi {
|
||||
blending: AlphaBlending,
|
||||
log: Log,
|
||||
completed: Arc<AtomicUsize>,
|
||||
surface: (u32, u32),
|
||||
}
|
||||
|
||||
struct MockTarget(usize, usize);
|
||||
impl GpuTarget for MockTarget {
|
||||
fn width(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
fn height(&self) -> usize {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
struct MockTexture {
|
||||
w: usize,
|
||||
h: usize,
|
||||
}
|
||||
impl GpuTexture for MockTexture {
|
||||
fn width(&self) -> usize {
|
||||
self.w
|
||||
}
|
||||
fn height(&self) -> usize {
|
||||
self.h
|
||||
}
|
||||
fn replace_region(
|
||||
&self,
|
||||
_x: usize,
|
||||
_y: usize,
|
||||
_w: usize,
|
||||
_h: usize,
|
||||
_data: &[u8],
|
||||
) -> Result<(), ApiError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockBuffer<T> {
|
||||
id: usize,
|
||||
len: usize,
|
||||
_marker: std::marker::PhantomData<fn() -> T>,
|
||||
}
|
||||
impl<T: Pod> GpuBuffer<MockApi, T> for MockBuffer<T> {
|
||||
fn raw(&self) -> usize {
|
||||
self.id
|
||||
}
|
||||
fn sync(&mut self, data: &[T]) -> Result<(), ApiError> {
|
||||
self.len = data.len();
|
||||
Ok(())
|
||||
}
|
||||
fn sync_from_slices(&mut self, lists: &[&[T]]) -> Result<usize, ApiError> {
|
||||
self.len = lists.iter().map(|l| l.len()).sum();
|
||||
Ok(self.len)
|
||||
}
|
||||
}
|
||||
|
||||
struct MockPipeline(&'static str);
|
||||
|
||||
struct MockShaders {
|
||||
pipelines: [MockPipeline; 5],
|
||||
post: Vec<MockPipeline>,
|
||||
}
|
||||
impl GpuShaders<MockApi> for MockShaders {
|
||||
fn bg_color(&self) -> &MockPipeline {
|
||||
&self.pipelines[0]
|
||||
}
|
||||
fn cell_bg(&self) -> &MockPipeline {
|
||||
&self.pipelines[1]
|
||||
}
|
||||
fn cell_text(&self) -> &MockPipeline {
|
||||
&self.pipelines[2]
|
||||
}
|
||||
fn image(&self) -> &MockPipeline {
|
||||
&self.pipelines[3]
|
||||
}
|
||||
fn bg_image(&self) -> &MockPipeline {
|
||||
&self.pipelines[4]
|
||||
}
|
||||
fn post_pipelines(&self) -> &[MockPipeline] {
|
||||
&self.post
|
||||
}
|
||||
}
|
||||
|
||||
struct MockFrame {
|
||||
log: Log,
|
||||
completed: Arc<AtomicUsize>,
|
||||
}
|
||||
struct MockPass {
|
||||
log: Log,
|
||||
}
|
||||
impl GpuRenderPass<'_, MockApi> for MockPass {
|
||||
fn step(&mut self, step: &Step<'_, MockApi>) {
|
||||
self.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(format!("{}:{}", step.pipeline.0, step.draw.instance_count));
|
||||
}
|
||||
fn complete(self) {
|
||||
self.log.lock().unwrap().push("pass-end".into());
|
||||
}
|
||||
}
|
||||
impl GpuFrame<MockApi> for MockFrame {
|
||||
fn render_pass<'a>(&'a self, _attachments: &'a [Attachment<'a, MockApi>]) -> MockPass
|
||||
where
|
||||
MockApi: 'a,
|
||||
{
|
||||
MockPass {
|
||||
log: self.log.clone(),
|
||||
}
|
||||
}
|
||||
fn complete(self, sync: bool, completion: FrameCompletion) {
|
||||
let completed = self.completed;
|
||||
let done = move || {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
completion(Health::Healthy);
|
||||
completed.fetch_add(1, Ordering::SeqCst);
|
||||
};
|
||||
if sync {
|
||||
done();
|
||||
} else {
|
||||
std::thread::spawn(done);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphicsApi for MockApi {
|
||||
const CUSTOM_SHADER_TARGET: shadertoy::Target = shadertoy::Target::Msl;
|
||||
const CUSTOM_SHADER_Y_IS_DOWN: bool = true;
|
||||
const SWAP_CHAIN_COUNT: usize = 3;
|
||||
|
||||
type Target = MockTarget;
|
||||
type Texture = MockTexture;
|
||||
type Sampler = ();
|
||||
type Buffer<T: Pod> = MockBuffer<T>;
|
||||
type BufferRef = usize;
|
||||
type Pipeline = MockPipeline;
|
||||
type Shaders = MockShaders;
|
||||
type GpuUniforms = Uniforms;
|
||||
type Frame = MockFrame;
|
||||
type RenderPass<'a> = MockPass;
|
||||
|
||||
fn gpu_uniforms(u: &Uniforms) -> Uniforms {
|
||||
*u
|
||||
}
|
||||
fn blending(&self) -> AlphaBlending {
|
||||
self.blending
|
||||
}
|
||||
fn set_blending(&mut self, b: AlphaBlending) {
|
||||
self.blending = b;
|
||||
}
|
||||
fn init_shaders(&self, _custom: &[String]) -> Result<MockShaders, ApiError> {
|
||||
Ok(MockShaders {
|
||||
pipelines: [
|
||||
MockPipeline("bg_color"),
|
||||
MockPipeline("cell_bg"),
|
||||
MockPipeline("cell_text"),
|
||||
MockPipeline("image"),
|
||||
MockPipeline("bg_image"),
|
||||
],
|
||||
post: Vec::new(),
|
||||
})
|
||||
}
|
||||
fn surface_size(&self) -> Result<(u32, u32), ApiError> {
|
||||
Ok(self.surface)
|
||||
}
|
||||
fn init_target(&self, w: usize, h: usize) -> Result<MockTarget, ApiError> {
|
||||
Ok(MockTarget(w, h))
|
||||
}
|
||||
fn init_buffer<T: Pod>(&self, len: usize) -> Result<MockBuffer<T>, ApiError> {
|
||||
Ok(MockBuffer {
|
||||
id: 1,
|
||||
len,
|
||||
_marker: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
fn init_buffer_fill<T: Pod>(&self, data: &[T]) -> Result<MockBuffer<T>, ApiError> {
|
||||
self.init_buffer(data.len())
|
||||
}
|
||||
fn init_texture(
|
||||
&self,
|
||||
_kind: TextureKind,
|
||||
w: usize,
|
||||
h: usize,
|
||||
_data: Option<&[u8]>,
|
||||
) -> Result<MockTexture, ApiError> {
|
||||
Ok(MockTexture { w, h })
|
||||
}
|
||||
fn init_atlas_texture(&self, atlas: &Atlas) -> Result<MockTexture, ApiError> {
|
||||
Ok(MockTexture {
|
||||
w: atlas.size as usize,
|
||||
h: atlas.size as usize,
|
||||
})
|
||||
}
|
||||
fn init_sampler(&self) -> Result<(), ApiError> {
|
||||
Ok(())
|
||||
}
|
||||
fn begin_frame(&self, _target: Arc<MockTarget>) -> MockFrame {
|
||||
MockFrame {
|
||||
log: self.log.clone(),
|
||||
completed: self.completed.clone(),
|
||||
}
|
||||
}
|
||||
fn present_last_target(&self) -> Result<(), ApiError> {
|
||||
Ok(())
|
||||
}
|
||||
fn clear_last_target(&self) {}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TestHost {
|
||||
health: Mutex<Vec<Health>>,
|
||||
}
|
||||
impl Host for TestHost {
|
||||
fn scrollbar(&self, _: Scrollbar) -> bool {
|
||||
true
|
||||
}
|
||||
fn renderer_health(&self, h: Health) {
|
||||
self.health.lock().unwrap().push(h);
|
||||
}
|
||||
fn redraw_surface(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture() -> (Renderer<MockApi>, Arc<Mutex<State>>, Log, Arc<AtomicUsize>) {
|
||||
let config = Config::default();
|
||||
let set = Arc::new(SharedGridSet::new().unwrap());
|
||||
let (_key, grid) = set
|
||||
.ref_(
|
||||
&FontDerivedConfig::new(&config),
|
||||
DesiredSize {
|
||||
points: 12.0,
|
||||
xdpi: 96,
|
||||
ydpi: 96,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let gc = grid.cell_size();
|
||||
let cell = CellSize {
|
||||
width: gc.width,
|
||||
height: gc.height,
|
||||
};
|
||||
let (w, h) = (cell.width * 20 + 20, cell.height * 5 + 20);
|
||||
let size = Size {
|
||||
screen: ScreenSize {
|
||||
width: w,
|
||||
height: h,
|
||||
},
|
||||
cell,
|
||||
padding: Padding {
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
},
|
||||
};
|
||||
|
||||
let mut term = Terminal::new(TermOptions::new(20, 5)).unwrap();
|
||||
term.vt_stream().next_slice(b"hello").unwrap();
|
||||
let state = State::new(term).into_shared();
|
||||
|
||||
let log = Log::default();
|
||||
let completed = Arc::new(AtomicUsize::new(0));
|
||||
let api = MockApi {
|
||||
blending: AlphaBlending::Native,
|
||||
log: log.clone(),
|
||||
completed: completed.clone(),
|
||||
surface: (w, h),
|
||||
};
|
||||
let renderer = Renderer::<MockApi>::new(
|
||||
Options {
|
||||
config: DerivedConfig::new(&config).unwrap(),
|
||||
font_grid: grid,
|
||||
size,
|
||||
host: Arc::new(TestHost::default()),
|
||||
},
|
||||
api,
|
||||
)
|
||||
.unwrap();
|
||||
(renderer, state, log, completed)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_triple_buffered_frames_complete_and_release() {
|
||||
let (renderer, state, log, completed) = fixture();
|
||||
|
||||
// More frames than the swap chain holds: `draw_frame` has to wait for
|
||||
// earlier frames to be handed back by their completion callbacks.
|
||||
for i in 0..10 {
|
||||
renderer.update_frame(&state, true).unwrap();
|
||||
renderer
|
||||
.draw_frame(true)
|
||||
.unwrap_or_else(|_| panic!("draw {i}"));
|
||||
}
|
||||
// sync = true completes inline, so each draw was fully completed.
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 10);
|
||||
|
||||
for _ in 0..10 {
|
||||
renderer.mark_dirty();
|
||||
renderer.update_frame(&state, true).unwrap();
|
||||
// Asynchronous completion on worker threads.
|
||||
renderer.draw_frame(false).unwrap();
|
||||
renderer.draw_frame(true).unwrap();
|
||||
}
|
||||
// Wait for the async completions.
|
||||
for _ in 0..200 {
|
||||
if completed.load(Ordering::SeqCst) >= 30 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 30);
|
||||
assert_eq!(renderer.health(), Health::Healthy);
|
||||
|
||||
// Every drawn frame ran bg_color, cell_bg and cell_text steps.
|
||||
let log = log.lock().unwrap();
|
||||
let bg = log.iter().filter(|l| l.starts_with("bg_color:")).count();
|
||||
let cell_bg = log.iter().filter(|l| l.starts_with("cell_bg:")).count();
|
||||
let text: Vec<_> = log.iter().filter(|l| l.starts_with("cell_text:")).collect();
|
||||
assert_eq!(bg, 30);
|
||||
assert_eq!(cell_bg, 30);
|
||||
assert_eq!(text.len(), 30);
|
||||
// "hello" is five glyph instances plus the cursor glyph.
|
||||
assert!(text.iter().all(|l| l.as_str() == "cell_text:6"), "{text:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_unrealize_waits_for_inflight_frames() {
|
||||
let (renderer, state, _log, completed) = fixture();
|
||||
renderer.update_frame(&state, true).unwrap();
|
||||
renderer.draw_frame(false).unwrap();
|
||||
// Deinitializing the swap chain waits until in-flight frames are back.
|
||||
renderer.display_unrealized();
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 1);
|
||||
renderer.display_realized_with(|_| {}).unwrap();
|
||||
renderer.draw_frame(true).unwrap();
|
||||
assert_eq!(completed.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
Reference in new issue
Block a user