renderer: add support for background images

Adds support for background images via the `background-image` config.

Resolves #3645, supersedes PRs #4226 and #5233.

See docs of added config keys for usage details.
This commit is contained in:
Qwerasd
2025-06-25 09:28:51 -06:00
committed by Mitchell Hashimoto
parent 03bdb92292
commit da46a47726
13 changed files with 871 additions and 10 deletions

View File

@ -31,8 +31,11 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation;
pub const RepeatableString = Config.RepeatableString;
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
pub const RepeatablePath = Config.RepeatablePath;
pub const Path = Config.Path;
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowPaddingColor = Config.WindowPaddingColor;
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
pub const BackgroundImageFit = Config.BackgroundImageFit;
// Alternate APIs
pub const CAPI = @import("config/CAPI.zig");

View File

@ -466,6 +466,84 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// Background image for the terminal.
///
/// This should be a path to a PNG or JPEG file,
/// other image formats are not yet supported.
@"background-image": ?Path = null,
/// Background image opacity.
///
/// This is relative to the value of `background-opacity`.
///
/// A value of `1.0` (the default) will result in the background image being
/// placed on top of the general background color, and then the combined result
/// will be adjusted to the opacity specified by `background-opacity`.
///
/// A value less than `1.0` will result in the background image being mixed
/// with the general background color before the combined result is adjusted
/// to the configured `background-opacity`.
///
/// A value greater than `1.0` will result in the background image having a
/// higher opacity than the general background color. For instance, if the
/// configured `background-opacity` is `0.5` and `background-image-opacity`
/// is set to `1.5`, then the final opacity of the background image will be
/// `0.5 * 1.5 = 0.75`.
@"background-image-opacity": f32 = 1.0,
/// Background image position.
///
/// Valid values are:
/// * `top-left`
/// * `top-center`
/// * `top-right`
/// * `center-left`
/// * `center`
/// * `center-right`
/// * `bottom-left`
/// * `bottom-center`
/// * `bottom-right`
///
/// The default value is `center`.
@"background-image-position": BackgroundImagePosition = .center,
/// Background image fit.
///
/// Valid values are:
///
/// * `contain`
///
/// Preserving the aspect ratio, scale the background image to the largest
/// size that can still be contained within the terminal, so that the whole
/// image is visible.
///
/// * `cover`
///
/// Preserving the aspect ratio, scale the background image to the smallest
/// size that can completely cover the terminal. This may result in one or
/// more edges of the image being clipped by the edge of the terminal.
///
/// * `stretch`
///
/// Stretch the background image to the full size of the terminal, without
/// preserving the aspect ratio.
///
/// * `none`
///
/// Don't scale the background image.
///
/// The default value is `contain`.
@"background-image-fit": BackgroundImageFit = .contain,
/// Whether to repeat the background image or not.
///
/// If this is set to true, the background image will be repeated if there
/// would otherwise be blank space around it because it doesn't completely
/// fill the terminal area.
///
/// The default value is `false`.
@"background-image-repeat": bool = false,
/// The foreground and background color for selection. If this is not set, then
/// the selection color is just the inverted window background and foreground
/// (note: not to be confused with the cell bg/fg).
@ -3298,6 +3376,15 @@ fn expandPaths(self: *Config, base: []const u8) !void {
&self._diagnostics,
);
},
?RepeatablePath, ?Path => {
if (@field(self, field.name)) |*path| {
try path.expand(
arena_alloc,
base,
&self._diagnostics,
);
}
},
else => {},
}
}
@ -6569,6 +6656,28 @@ pub const AlphaBlending = enum {
}
};
/// See background-image-position
pub const BackgroundImagePosition = enum {
@"top-left",
@"top-center",
@"top-right",
@"center-left",
@"center-center",
@"center-right",
@"bottom-left",
@"bottom-center",
@"bottom-right",
center,
};
/// See background-image-fit
pub const BackgroundImageFit = enum {
contain,
cover,
stretch,
none,
};
/// See freetype-load-flag
pub const FreetypeLoadFlags = packed struct {
// The defaults here at the time of writing this match the defaults

View File

@ -282,6 +282,7 @@ pub const uniformBufferOptions = bufferOptions;
pub const fgBufferOptions = bufferOptions;
pub const bgBufferOptions = bufferOptions;
pub const imageBufferOptions = bufferOptions;
pub const bgImageBufferOptions = bufferOptions;
/// Returns the options to use when constructing textures.
pub inline fn textureOptions(self: Metal) Texture.Options {

View File

@ -388,6 +388,7 @@ pub const uniformBufferOptions = bufferOptions;
pub const fgBufferOptions = bufferOptions;
pub const bgBufferOptions = bufferOptions;
pub const imageBufferOptions = bufferOptions;
pub const bgImageBufferOptions = bufferOptions;
/// Returns the options to use when constructing textures.
pub inline fn textureOptions(self: OpenGL) Texture.Options {

View File

@ -2,6 +2,7 @@ const std = @import("std");
const builtin = @import("builtin");
const glfw = @import("glfw");
const xev = @import("xev");
const wuffs = @import("wuffs");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");
@ -25,6 +26,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const Terminal = terminal.Terminal;
const Health = renderer.Health;
const FileType = @import("../file_type.zig").FileType;
const macos = switch (builtin.os.tag) {
.macos => @import("macos"),
else => void,
@ -181,6 +184,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
image_text_end: u32 = 0,
image_virtual: bool = false,
/// Background image, if we have one.
bg_image: ?imagepkg.Image = null,
/// Set whenever the background image changes, singalling
/// that the new background image needs to be uploaded to
/// the GPU.
///
/// This is initialized as true so that we load the image
/// on renderer initialization, not just on config change.
bg_image_changed: bool = true,
/// Background image vertex buffer.
bg_image_buffer: shaderpkg.BgImage,
/// This value is used to force-update the swap chain copy
/// of the background image buffer whenever we change it.
bg_image_buffer_modified: usize = 0,
/// Graphics API state.
api: GraphicsAPI,
@ -298,12 +316,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// See property of same name on Renderer for explanation.
target_config_modified: usize = 0,
/// Buffer with the vertex data for our background image.
///
/// TODO: Make this an optional and only create it
/// if we actually have a background image.
bg_image_buffer: BgImageBuffer,
/// See property of same name on Renderer for explanation.
bg_image_buffer_modified: usize = 0,
/// Custom shader state, this is null if we have no custom shaders.
custom_shader_state: ?CustomShaderState = null,
const UniformBuffer = Buffer(shaderpkg.Uniforms);
const CellBgBuffer = Buffer(shaderpkg.CellBg);
const CellTextBuffer = Buffer(shaderpkg.CellText);
const BgImageBuffer = Buffer(shaderpkg.BgImage);
pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
// Uniform buffer contains exactly 1 uniform struct. The
@ -323,6 +350,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1);
errdefer cells_bg.deinit();
// Create a GPU buffer for our background image info.
var bg_image_buffer = try BgImageBuffer.init(
api.bgImageBufferOptions(),
1,
);
errdefer bg_image_buffer.deinit();
// Initialize our textures for our font atlas.
//
// As with the buffers above, we start these off as small
@ -355,6 +389,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.uniforms = uniforms,
.cells = cells,
.cells_bg = cells_bg,
.bg_image_buffer = bg_image_buffer,
.grayscale = grayscale,
.color = color,
.target = target,
@ -368,6 +403,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.cells_bg.deinit();
self.grayscale.deinit();
self.color.deinit();
self.bg_image_buffer.deinit();
if (self.custom_shader_state) |*state| state.deinit();
}
@ -491,6 +527,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
bg_image: ?configpkg.Path,
bg_image_opacity: f32,
bg_image_position: configpkg.BackgroundImagePosition,
bg_image_fit: configpkg.BackgroundImageFit,
bg_image_repeat: bool,
links: link.Set,
vsync: bool,
colorspace: configpkg.Config.WindowColorspace,
@ -507,6 +548,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Copy our shaders
const custom_shaders = try config.@"custom-shader".clone(alloc);
// Copy our background image
const bg_image =
if (config.@"background-image") |bg|
try bg.clone(alloc)
else
null;
// Copy our font features
const font_features = try config.@"font-feature".clone(alloc);
@ -563,6 +611,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
null,
.custom_shaders = custom_shaders,
.bg_image = bg_image,
.bg_image_opacity = config.@"background-image-opacity",
.bg_image_position = config.@"background-image-position",
.bg_image_fit = config.@"background-image-fit",
.bg_image_repeat = config.@"background-image-repeat",
.links = links,
.vsync = config.@"window-vsync",
.colorspace = config.@"window-colorspace",
@ -657,6 +710,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.cell_size = undefined,
.grid_size = undefined,
.grid_padding = undefined,
.screen_size = undefined,
.padding_extend = .{},
.min_contrast = options.config.min_contrast,
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
@ -691,6 +745,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.previous_cursor_color = @splat(0),
.cursor_change_time = 0,
},
.bg_image_buffer = undefined,
// Fonts
.font_grid = options.font_grid,
@ -711,6 +766,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Ensure our undefined values above are correctly initialized.
result.updateFontGridUniforms();
result.updateScreenSizeUniforms();
result.updateBgImageBuffer();
try result.prepBackgroundImage();
return result;
}
@ -739,6 +796,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
self.image_placements.deinit(self.alloc);
if (self.bg_image) |img| img.deinit(self.alloc);
self.deinitShaders();
self.api.deinit();
@ -1336,6 +1395,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Upload images to the GPU as necessary.
try self.uploadKittyImages();
// Upload the background image to the GPU as necessary.
try self.uploadBackgroundImage();
// Update custom shader uniforms if necessary.
try self.updateCustomShaderUniforms();
@ -1344,6 +1406,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
try frame.cells_bg.sync(self.cells.bg_cells);
const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
// If our background image buffer has changed, sync it.
if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) {
try frame.bg_image_buffer.sync(&.{self.bg_image_buffer});
frame.bg_image_buffer_modified = self.bg_image_buffer_modified;
}
// If our font atlas changed, sync the texture data
texture: {
const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
@ -1376,18 +1445,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}});
defer pass.complete();
// First we draw the background color.
// First we draw our background image, if we have one.
// The bg image shader also draws the main bg color.
//
// Otherwise, if we don't have a background image, we
// draw the background color by itself in its own step.
//
// NOTE: We don't use the clear_color for this because that
// would require us to do color space conversion on the
// CPU-side. In the future when we have utilities for
// that we should remove this step and use clear_color.
if (self.bg_image) |img| switch (img) {
.ready => |texture| pass.step(.{
.pipeline = self.shaders.pipelines.bg_image,
.uniforms = frame.uniforms.buffer,
.buffers = &.{frame.bg_image_buffer.buffer},
.textures = &.{texture},
.draw = .{ .type = .triangle, .vertex_count = 3 },
}),
else => {},
} else {
pass.step(.{
.pipeline = self.shaders.pipelines.bg_color,
.uniforms = frame.uniforms.buffer,
.buffers = &.{ null, frame.cells_bg.buffer },
.draw = .{ .type = .triangle, .vertex_count = 3 },
});
}
// Then we draw any kitty images that need
// to be behind text AND cell backgrounds.
@ -1863,6 +1947,102 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
}
/// Call this any time the background image path changes.
///
/// Caller must hold the draw mutex.
fn prepBackgroundImage(self: *Self) !void {
// Then we try to load the background image if we have a path.
if (self.config.bg_image) |p| load_background: {
const path = switch (p) {
.required, .optional => |slice| slice,
};
// Open the file
var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
log.warn(
"error opening background image file \"{s}\": {}",
.{ path, err },
);
break :load_background;
};
defer file.close();
// Read it
const contents = file.readToEndAlloc(
self.alloc,
std.math.maxInt(u32), // Max size of 4 GiB, for now.
) catch |err| {
log.warn(
"error reading background image file \"{s}\": {}",
.{ path, err },
);
break :load_background;
};
defer self.alloc.free(contents);
// Figure out what type it probably is.
const file_type = switch (FileType.detect(contents)) {
.unknown => FileType.guessFromExtension(
std.fs.path.extension(path),
),
else => |t| t,
};
// Decode it if we know how.
const image_data = switch (file_type) {
.png => try wuffs.png.decode(self.alloc, contents),
.jpeg => try wuffs.jpeg.decode(self.alloc, contents),
.unknown => {
log.warn(
"Cannot determine file type for background image file \"{s}\"!",
.{path},
);
break :load_background;
},
else => |f| {
log.warn(
"Unsupported file type {} for background image file \"{s}\"!",
.{ f, path },
);
break :load_background;
},
};
const image: imagepkg.Image = .{
.pending = .{
.width = image_data.width,
.height = image_data.height,
.pixel_format = .rgba,
.data = image_data.data.ptr,
},
};
// If we have an existing background image, replace it.
// Otherwise, set this as our background image directly.
if (self.bg_image) |*img| {
try img.markForReplace(self.alloc, image);
} else {
self.bg_image = image;
}
} else {
// If we don't have a background image path, mark our
// background image for unload if we currently have one.
if (self.bg_image) |*img| img.markForUnload();
}
}
fn uploadBackgroundImage(self: *Self) !void {
// Make sure our bg image is uploaded if it needs to be.
if (self.bg_image) |*bg| {
if (bg.isUnloading()) {
bg.deinit(self.alloc);
self.bg_image = null;
return;
}
if (bg.isPending()) try bg.upload(self.alloc, &self.api);
}
}
/// Update the configuration.
pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
self.draw_mutex.lock();
@ -1900,12 +2080,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
const bg_image_config_changed =
self.config.bg_image_fit != config.bg_image_fit or
self.config.bg_image_position != config.bg_image_position or
self.config.bg_image_repeat != config.bg_image_repeat or
self.config.bg_image_opacity != config.bg_image_opacity;
const bg_image_changed =
if (self.config.bg_image) |old|
if (config.bg_image) |new|
!old.equal(new)
else
true
else
config.bg_image != null;
const old_blending = self.config.blending;
const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
self.config.deinit();
self.config = config.*;
// If our background image path changed, prepare the new bg image.
if (bg_image_changed) try self.prepBackgroundImage();
// If our background image config changed, update the vertex buffer.
if (bg_image_config_changed) self.updateBgImageBuffer();
// Reset our viewport to force a rebuild, in case of a font change.
self.cells_viewport = null;
@ -1975,14 +2176,50 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
@floatFromInt(blank.bottom),
@floatFromInt(blank.left),
};
self.uniforms.screen_size = .{
@floatFromInt(self.size.screen.width),
@floatFromInt(self.size.screen.height),
};
}
/// Update the background image vertex buffer (CPU-side).
///
/// This should be called if and when configs change that
/// could affect the background image.
///
/// Caller must hold the draw mutex.
fn updateBgImageBuffer(self: *Self) void {
self.bg_image_buffer = .{
.opacity = self.config.bg_image_opacity,
.info = .{
.position = switch (self.config.bg_image_position) {
.@"top-left" => .tl,
.@"top-center" => .tc,
.@"top-right" => .tr,
.@"center-left" => .ml,
.@"center-center", .center => .mc,
.@"center-right" => .mr,
.@"bottom-left" => .bl,
.@"bottom-center" => .bc,
.@"bottom-right" => .br,
},
.fit = switch (self.config.bg_image_fit) {
.contain => .contain,
.cover => .cover,
.stretch => .stretch,
.none => .none,
},
.repeat = self.config.bg_image_repeat,
},
};
// Signal that the buffer was modified.
self.bg_image_buffer_modified +%= 1;
}
/// Update uniforms for the custom shaders, if necessary.
///
/// This should be called exactly once per frame, inside `drawFrame`.
fn updateCustomShaderUniforms(
self: *Self,
) !void {
fn updateCustomShaderUniforms(self: *Self) !void {
// We only need to do this if we have custom shaders.
if (!self.has_custom_shaders) return;

View File

@ -160,6 +160,7 @@ fn autoAttribute(T: type, attrs: objc.Object) void {
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"struct" => |e| e.backing_integer.?,
.@"enum" => |e| e.tag_type,
else => field.type,
};
@ -169,13 +170,17 @@ fn autoAttribute(T: type, attrs: objc.Object) void {
[4]u8 => mtl.MTLVertexFormat.uchar4,
[2]u16 => mtl.MTLVertexFormat.ushort2,
[2]i16 => mtl.MTLVertexFormat.short2,
f32 => mtl.MTLVertexFormat.float,
[2]f32 => mtl.MTLVertexFormat.float2,
[4]f32 => mtl.MTLVertexFormat.float4,
i32 => mtl.MTLVertexFormat.int,
[2]i32 => mtl.MTLVertexFormat.int2,
[4]i32 => mtl.MTLVertexFormat.int2,
u32 => mtl.MTLVertexFormat.uint,
[2]u32 => mtl.MTLVertexFormat.uint2,
[4]u32 => mtl.MTLVertexFormat.uint4,
u8 => mtl.MTLVertexFormat.uchar,
i8 => mtl.MTLVertexFormat.char,
else => comptime unreachable,
};

View File

@ -36,6 +36,13 @@ const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
.step_fn = .per_instance,
.blending_enabled = true,
} },
.{ "bg_image", .{
.vertex_attributes = BgImage,
.vertex_fn = "bg_image_vertex",
.fragment_fn = "bg_image_fragment",
.step_fn = .per_instance,
.blending_enabled = true,
} },
};
/// All the comptime-known info about a pipeline, so that
@ -192,6 +199,9 @@ pub const Uniforms = extern struct {
/// This is calculated based on the size of the screen.
projection_matrix: math.Mat align(16),
/// Size of the screen (render target) in pixels.
screen_size: [2]f32 align(8),
/// Size of a single cell in pixels, unscaled.
cell_size: [2]f32 align(8),
@ -288,6 +298,38 @@ pub const Image = extern struct {
dest_size: [2]f32,
};
/// Single parameter for the bg image shader.
pub const BgImage = extern struct {
opacity: f32 align(4),
info: Info align(1),
pub const Info = packed struct(u8) {
position: Position,
fit: Fit,
repeat: bool,
_padding: u1 = 0,
pub const Position = enum(u4) {
tl = 0,
tc = 1,
tr = 2,
ml = 3,
mc = 4,
mr = 5,
bl = 6,
bc = 7,
br = 8,
};
pub const Fit = enum(u2) {
contain = 0,
cover = 1,
stretch = 2,
none = 3,
};
};
};
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
fn initLibrary(device: objc.Object) !objc.Object {
const start = try std.time.Instant.now();

View File

@ -98,6 +98,7 @@ fn autoAttribute(
const offset = @offsetOf(T, field.name);
const FT = switch (@typeInfo(field.type)) {
.@"struct" => |s| s.backing_integer.?,
.@"enum" => |e| e.tag_type,
else => field.type,
};

View File

@ -33,6 +33,13 @@ const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
.step_fn = .per_instance,
.blending_enabled = true,
} },
.{ "bg_image", .{
.vertex_attributes = BgImage,
.vertex_fn = loadShaderCode("../shaders/glsl/bg_image.v.glsl"),
.fragment_fn = loadShaderCode("../shaders/glsl/bg_image.f.glsl"),
.step_fn = .per_instance,
.blending_enabled = true,
} },
};
/// All the comptime-known info about a pipeline, so that
@ -158,6 +165,9 @@ pub const Uniforms = extern struct {
/// This is calculated based on the size of the screen.
projection_matrix: math.Mat align(16),
/// Size of the screen (render target) in pixels.
screen_size: [2]f32 align(8),
/// Size of a single cell in pixels, unscaled.
cell_size: [2]f32 align(8),
@ -256,6 +266,38 @@ pub const Image = extern struct {
dest_size: [2]f32 align(8),
};
/// Single parameter for the bg image shader.
pub const BgImage = extern struct {
opacity: f32 align(4),
info: Info align(1),
pub const Info = packed struct(u8) {
position: Position,
fit: Fit,
repeat: bool,
_padding: u1 = 0,
pub const Position = enum(u4) {
tl = 0,
tc = 1,
tr = 2,
ml = 3,
mc = 4,
mr = 5,
bl = 6,
bc = 7,
br = 8,
};
pub const Fit = enum(u2) {
contain = 0,
cover = 1,
stretch = 2,
none = 3,
};
};
};
/// Initialize our custom shader pipelines. The shaders argument is a
/// set of shader source code, not file paths.
fn initPostPipelines(

View File

@ -0,0 +1,62 @@
#include "common.glsl"
// Position the FragCoord origin to the upper left
// so as to align with our texture's directionality.
layout(origin_upper_left) in vec4 gl_FragCoord;
layout(binding = 0) uniform sampler2DRect image;
flat in vec4 bg_color;
flat in vec2 offset;
flat in vec2 scale;
flat in float opacity;
flat in uint repeat;
layout(location = 0) out vec4 out_FragColor;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
// 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.
vec2 tex_coord = (gl_FragCoord.xy - offset) * scale;
vec2 tex_size = textureSize(image);
// If we need to repeat the texture, wrap the coordinates.
if (repeat != 0) {
tex_coord = mod(mod(tex_coord, tex_size) + tex_size, tex_size);
}
vec4 rgba;
// If we're out of bounds, we have no color,
// otherwise we sample the texture for it.
if (any(lessThan(tex_coord, vec2(0.0))) ||
any(greaterThan(tex_coord, tex_size)))
{
rgba = vec4(0.0);
} else {
rgba = texture(image, tex_coord);
if (!use_linear_blending) {
rgba = unlinearize(rgba);
}
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(opacity, 1.0 / bg_color.a);
// Blend it on to a fully opaque version of the background color.
rgba += max(vec4(0.0), vec4(bg_color.rgb, 1.0) * vec4(1.0 - rgba.a));
// Multiply everything by the background color alpha.
rgba *= bg_color.a;
out_FragColor = rgba;
}

View File

@ -0,0 +1,145 @@
#include "common.glsl"
layout(binding = 0) uniform sampler2DRect image;
layout(location = 0) in float in_opacity;
layout(location = 1) in uint info;
// 4 bits of info.
const uint BG_IMAGE_POSITION = 15u;
const uint BG_IMAGE_TL = 0u;
const uint BG_IMAGE_TC = 1u;
const uint BG_IMAGE_TR = 2u;
const uint BG_IMAGE_ML = 3u;
const uint BG_IMAGE_MC = 4u;
const uint BG_IMAGE_MR = 5u;
const uint BG_IMAGE_BL = 6u;
const uint BG_IMAGE_BC = 7u;
const uint BG_IMAGE_BR = 8u;
// 2 bits of info shifted 4.
const uint BG_IMAGE_FIT = 3u << 4;
const uint BG_IMAGE_CONTAIN = 0u << 4;
const uint BG_IMAGE_COVER = 1u << 4;
const uint BG_IMAGE_STRETCH = 2u << 4;
const uint BG_IMAGE_NO_FIT = 3u << 4;
// 1 bit of info shifted 6.
const uint BG_IMAGE_REPEAT = 1u << 6;
flat out vec4 bg_color;
flat out vec2 offset;
flat out vec2 scale;
flat out float opacity;
// We use a uint to pass the repeat value because
// bools aren't allowed for vertex outputs in OpenGL.
flat out uint repeat;
void main() {
bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
vec4 position;
position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
position.z = 1.0;
position.w = 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)
gl_Position = position;
opacity = in_opacity;
repeat = info & BG_IMAGE_REPEAT;
vec2 screen_size = screen_size;
vec2 tex_size = textureSize(image);
vec2 dest_size = tex_size;
switch (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;
}
vec2 start = vec2(0.0);
vec2 mid = (screen_size - dest_size) / vec2(2.0);
vec2 end = screen_size - dest_size;
vec2 dest_offset = mid;
switch (info & BG_IMAGE_POSITION) {
case BG_IMAGE_TL: {
dest_offset = vec2(start.x, start.y);
} break;
case BG_IMAGE_TC: {
dest_offset = vec2(mid.x, start.y);
} break;
case BG_IMAGE_TR: {
dest_offset = vec2(end.x, start.y);
} break;
case BG_IMAGE_ML: {
dest_offset = vec2(start.x, mid.y);
} break;
case BG_IMAGE_MC: {
dest_offset = vec2(mid.x, mid.y);
} break;
case BG_IMAGE_MR: {
dest_offset = vec2(end.x, mid.y);
} break;
case BG_IMAGE_BL: {
dest_offset = vec2(start.x, end.y);
} break;
case BG_IMAGE_BC: {
dest_offset = vec2(mid.x, end.y);
} break;
case BG_IMAGE_BR: {
dest_offset = vec2(end.x, end.y);
} break;
}
offset = dest_offset;
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.
uvec4 u_bg_color = unpack4u8(bg_color_packed_4u8);
bg_color = vec4(load_color(
uvec4(u_bg_color.rgb, 255),
use_linear_blending
).rgb, float(u_bg_color.a) / 255.0);
}

View File

@ -13,6 +13,7 @@
//----------------------------------------------------------------------------//
layout(binding = 1, std140) uniform Globals {
uniform mat4 projection_matrix;
uniform vec2 screen_size;
uniform vec2 cell_size;
uniform uint grid_size_packed_2u16;
uniform vec4 grid_padding;

View File

@ -11,6 +11,7 @@ enum Padding : uint8_t {
struct Uniforms {
float4x4 projection_matrix;
float2 screen_size;
float2 cell_size;
ushort2 grid_size;
float4 grid_padding;
@ -231,6 +232,217 @@ fragment float4 bg_color_fragment(
);
}
//-------------------------------------------------------------------
// 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
//-------------------------------------------------------------------