Terminal Background Image Support (#7686)
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. > [!NOTE] > Unlike what is implied by the original issue, because this is implemented in the renderer it is inherently per-surface not per-window, meaning a window with a split will have two copies of the background image. ### Future work - We should probably introduce code in the apprts that tells surfaces their position and size relative to the window, which would allow us to add a `background-image-area` config with options for `surface` and `window` to control that behavior (and probably default it to `window`). That apprt code would also allow for window-relative custom shader locations, which is also a fairly common user request, so I think it's worth it. - Currently if you use a high res background image this is fairly inefficient, since each surface independently loads a copy of the background image. On systems with limited VRAM this could be an issue for users who use a lot of surfaces, so it may be worth making a shared image cache to avoid this problem. - ~~It's probably worth using compressed texture formats for images, I'll look in to doing that.~~ (c43702c)pull/7692/head
commit
9eec80e038
|
|
@ -74,6 +74,9 @@ pub const InternalFormat = enum(c_int) {
|
||||||
srgb = c.GL_SRGB8,
|
srgb = c.GL_SRGB8,
|
||||||
srgba = c.GL_SRGB8_ALPHA8,
|
srgba = c.GL_SRGB8_ALPHA8,
|
||||||
|
|
||||||
|
rgba_compressed = c.GL_COMPRESSED_RGBA_BPTC_UNORM,
|
||||||
|
srgba_compressed = c.GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM,
|
||||||
|
|
||||||
// There are so many more that I haven't filled in.
|
// There are so many more that I haven't filled in.
|
||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
@ -126,7 +129,6 @@ pub const Binding = struct {
|
||||||
internal_format: InternalFormat,
|
internal_format: InternalFormat,
|
||||||
width: c.GLsizei,
|
width: c.GLsizei,
|
||||||
height: c.GLsizei,
|
height: c.GLsizei,
|
||||||
border: c.GLint,
|
|
||||||
format: Format,
|
format: Format,
|
||||||
typ: DataType,
|
typ: DataType,
|
||||||
data: ?*const anyopaque,
|
data: ?*const anyopaque,
|
||||||
|
|
@ -137,7 +139,7 @@ pub const Binding = struct {
|
||||||
@intFromEnum(internal_format),
|
@intFromEnum(internal_format),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
border,
|
0,
|
||||||
@intFromEnum(format),
|
@intFromEnum(format),
|
||||||
@intFromEnum(typ),
|
@intFromEnum(typ),
|
||||||
data,
|
data,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pub const Error = @import("error.zig").Error;
|
||||||
pub const ImageData = struct {
|
pub const ImageData = struct {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
data: []const u8,
|
data: []u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,24 @@ pub fn rgbToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bgrToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
|
||||||
|
return swizzle(
|
||||||
|
alloc,
|
||||||
|
src,
|
||||||
|
c.WUFFS_BASE__PIXEL_FORMAT__BGR,
|
||||||
|
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bgraToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
|
||||||
|
return swizzle(
|
||||||
|
alloc,
|
||||||
|
src,
|
||||||
|
c.WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL,
|
||||||
|
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn swizzle(
|
fn swizzle(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
src: []const u8,
|
src: []const u8,
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,11 @@ pub const RepeatableFontVariation = Config.RepeatableFontVariation;
|
||||||
pub const RepeatableString = Config.RepeatableString;
|
pub const RepeatableString = Config.RepeatableString;
|
||||||
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
|
pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig");
|
||||||
pub const RepeatablePath = Config.RepeatablePath;
|
pub const RepeatablePath = Config.RepeatablePath;
|
||||||
|
pub const Path = Config.Path;
|
||||||
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
|
||||||
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
pub const WindowPaddingColor = Config.WindowPaddingColor;
|
||||||
|
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
|
||||||
|
pub const BackgroundImageFit = Config.BackgroundImageFit;
|
||||||
|
|
||||||
// Alternate APIs
|
// Alternate APIs
|
||||||
pub const CAPI = @import("config/CAPI.zig");
|
pub const CAPI = @import("config/CAPI.zig");
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,93 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
|
||||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||||
foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
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.
|
||||||
|
///
|
||||||
|
/// The background image is currently per-terminal, not per-window. If
|
||||||
|
/// you are a heavy split user, the background image will be repeated across
|
||||||
|
/// splits. A future improvement to Ghostty will address this.
|
||||||
|
///
|
||||||
|
/// WARNING: Background images are currently duplicated in VRAM per-terminal.
|
||||||
|
/// For sufficiently large images, this could lead to a large increase in
|
||||||
|
/// memory usage (specifically VRAM usage). A future Ghostty improvement
|
||||||
|
/// will resolve this by sharing image textures across terminals.
|
||||||
|
@"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 foreground and background color for selection. If this is not set, then
|
||||||
/// the selection color is just the inverted window background and foreground
|
/// the selection color is just the inverted window background and foreground
|
||||||
/// (note: not to be confused with the cell bg/fg).
|
/// (note: not to be confused with the cell bg/fg).
|
||||||
|
|
@ -3298,6 +3385,15 @@ fn expandPaths(self: *Config, base: []const u8) !void {
|
||||||
&self._diagnostics,
|
&self._diagnostics,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
?RepeatablePath, ?Path => {
|
||||||
|
if (@field(self, field.name)) |*path| {
|
||||||
|
try path.expand(
|
||||||
|
arena_alloc,
|
||||||
|
base,
|
||||||
|
&self._diagnostics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6569,6 +6665,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
|
/// See freetype-load-flag
|
||||||
pub const FreetypeLoadFlags = packed struct {
|
pub const FreetypeLoadFlags = packed struct {
|
||||||
// The defaults here at the time of writing this match the defaults
|
// The defaults here at the time of writing this match the defaults
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const type_details: []const struct {
|
||||||
|
typ: FileType,
|
||||||
|
sigs: []const []const ?u8,
|
||||||
|
exts: []const []const u8,
|
||||||
|
} = &.{
|
||||||
|
.{
|
||||||
|
.typ = .jpeg,
|
||||||
|
.sigs = &.{
|
||||||
|
&.{ 0xFF, 0xD8, 0xFF, 0xDB },
|
||||||
|
&.{ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01 },
|
||||||
|
&.{ 0xFF, 0xD8, 0xFF, 0xEE },
|
||||||
|
&.{ 0xFF, 0xD8, 0xFF, 0xE1, null, null, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 },
|
||||||
|
&.{ 0xFF, 0xD8, 0xFF, 0xE0 },
|
||||||
|
},
|
||||||
|
.exts = &.{ ".jpg", ".jpeg", ".jfif" },
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.typ = .png,
|
||||||
|
.sigs = &.{&.{ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }},
|
||||||
|
.exts = &.{".png"},
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.typ = .gif,
|
||||||
|
.sigs = &.{
|
||||||
|
&.{ 'G', 'I', 'F', '8', '7', 'a' },
|
||||||
|
&.{ 'G', 'I', 'F', '8', '9', 'a' },
|
||||||
|
},
|
||||||
|
.exts = &.{".gif"},
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.typ = .bmp,
|
||||||
|
.sigs = &.{&.{ 'B', 'M' }},
|
||||||
|
.exts = &.{".bmp"},
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.typ = .qoi,
|
||||||
|
.sigs = &.{&.{ 'q', 'o', 'i', 'f' }},
|
||||||
|
.exts = &.{".qoi"},
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.typ = .webp,
|
||||||
|
.sigs = &.{
|
||||||
|
&.{ 0x52, 0x49, 0x46, 0x46, null, null, null, null, 0x57, 0x45, 0x42, 0x50 },
|
||||||
|
},
|
||||||
|
.exts = &.{".webp"},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This is a helper for detecting file types based on magic bytes.
|
||||||
|
///
|
||||||
|
/// Ref: https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||||
|
pub const FileType = enum {
|
||||||
|
/// JPEG image file.
|
||||||
|
jpeg,
|
||||||
|
|
||||||
|
/// PNG image file.
|
||||||
|
png,
|
||||||
|
|
||||||
|
/// GIF image file.
|
||||||
|
gif,
|
||||||
|
|
||||||
|
/// BMP image file.
|
||||||
|
bmp,
|
||||||
|
|
||||||
|
/// QOI image file.
|
||||||
|
qoi,
|
||||||
|
|
||||||
|
/// WebP image file.
|
||||||
|
webp,
|
||||||
|
|
||||||
|
/// Unknown file format.
|
||||||
|
unknown,
|
||||||
|
|
||||||
|
/// Detect file type based on the magic bytes
|
||||||
|
/// at the start of the provided file contents.
|
||||||
|
pub fn detect(contents: []const u8) FileType {
|
||||||
|
inline for (type_details) |typ| {
|
||||||
|
inline for (typ.sigs) |signature| {
|
||||||
|
if (contents.len >= signature.len) {
|
||||||
|
for (contents[0..signature.len], signature) |f, sig| {
|
||||||
|
if (sig) |s| if (f != s) break;
|
||||||
|
} else {
|
||||||
|
return typ.typ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guess file type from its extension.
|
||||||
|
pub fn guessFromExtension(extension: []const u8) FileType {
|
||||||
|
inline for (type_details) |typ| {
|
||||||
|
inline for (typ.exts) |ext| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(extension, ext)) return typ.typ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .unknown;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -282,6 +282,7 @@ pub const uniformBufferOptions = bufferOptions;
|
||||||
pub const fgBufferOptions = bufferOptions;
|
pub const fgBufferOptions = bufferOptions;
|
||||||
pub const bgBufferOptions = bufferOptions;
|
pub const bgBufferOptions = bufferOptions;
|
||||||
pub const imageBufferOptions = bufferOptions;
|
pub const imageBufferOptions = bufferOptions;
|
||||||
|
pub const bgImageBufferOptions = bufferOptions;
|
||||||
|
|
||||||
/// Returns the options to use when constructing textures.
|
/// Returns the options to use when constructing textures.
|
||||||
pub inline fn textureOptions(self: Metal) Texture.Options {
|
pub inline fn textureOptions(self: Metal) Texture.Options {
|
||||||
|
|
|
||||||
|
|
@ -388,13 +388,14 @@ pub const uniformBufferOptions = bufferOptions;
|
||||||
pub const fgBufferOptions = bufferOptions;
|
pub const fgBufferOptions = bufferOptions;
|
||||||
pub const bgBufferOptions = bufferOptions;
|
pub const bgBufferOptions = bufferOptions;
|
||||||
pub const imageBufferOptions = bufferOptions;
|
pub const imageBufferOptions = bufferOptions;
|
||||||
|
pub const bgImageBufferOptions = bufferOptions;
|
||||||
|
|
||||||
/// Returns the options to use when constructing textures.
|
/// Returns the options to use when constructing textures.
|
||||||
pub inline fn textureOptions(self: OpenGL) Texture.Options {
|
pub inline fn textureOptions(self: OpenGL) Texture.Options {
|
||||||
_ = self;
|
_ = self;
|
||||||
return .{
|
return .{
|
||||||
.format = .rgba,
|
.format = .rgba,
|
||||||
.internal_format = .srgba,
|
.internal_format = .srgba_compressed,
|
||||||
.target = .@"2D",
|
.target = .@"2D",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
const xev = @import("xev");
|
const xev = @import("xev");
|
||||||
|
const wuffs = @import("wuffs");
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const configpkg = @import("../config.zig");
|
const configpkg = @import("../config.zig");
|
||||||
const font = @import("../font/main.zig");
|
const font = @import("../font/main.zig");
|
||||||
|
|
@ -25,6 +26,8 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
const Terminal = terminal.Terminal;
|
const Terminal = terminal.Terminal;
|
||||||
const Health = renderer.Health;
|
const Health = renderer.Health;
|
||||||
|
|
||||||
|
const FileType = @import("../file_type.zig").FileType;
|
||||||
|
|
||||||
const macos = switch (builtin.os.tag) {
|
const macos = switch (builtin.os.tag) {
|
||||||
.macos => @import("macos"),
|
.macos => @import("macos"),
|
||||||
else => void,
|
else => void,
|
||||||
|
|
@ -181,6 +184,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
image_text_end: u32 = 0,
|
image_text_end: u32 = 0,
|
||||||
image_virtual: bool = false,
|
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.
|
/// Graphics API state.
|
||||||
api: GraphicsAPI,
|
api: GraphicsAPI,
|
||||||
|
|
||||||
|
|
@ -298,12 +316,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
/// See property of same name on Renderer for explanation.
|
/// See property of same name on Renderer for explanation.
|
||||||
target_config_modified: usize = 0,
|
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, this is null if we have no custom shaders.
|
||||||
custom_shader_state: ?CustomShaderState = null,
|
custom_shader_state: ?CustomShaderState = null,
|
||||||
|
|
||||||
const UniformBuffer = Buffer(shaderpkg.Uniforms);
|
const UniformBuffer = Buffer(shaderpkg.Uniforms);
|
||||||
const CellBgBuffer = Buffer(shaderpkg.CellBg);
|
const CellBgBuffer = Buffer(shaderpkg.CellBg);
|
||||||
const CellTextBuffer = Buffer(shaderpkg.CellText);
|
const CellTextBuffer = Buffer(shaderpkg.CellText);
|
||||||
|
const BgImageBuffer = Buffer(shaderpkg.BgImage);
|
||||||
|
|
||||||
pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
|
pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
|
||||||
// Uniform buffer contains exactly 1 uniform struct. The
|
// 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);
|
var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1);
|
||||||
errdefer cells_bg.deinit();
|
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.
|
// Initialize our textures for our font atlas.
|
||||||
//
|
//
|
||||||
// As with the buffers above, we start these off as small
|
// As with the buffers above, we start these off as small
|
||||||
|
|
@ -355,6 +389,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
.uniforms = uniforms,
|
.uniforms = uniforms,
|
||||||
.cells = cells,
|
.cells = cells,
|
||||||
.cells_bg = cells_bg,
|
.cells_bg = cells_bg,
|
||||||
|
.bg_image_buffer = bg_image_buffer,
|
||||||
.grayscale = grayscale,
|
.grayscale = grayscale,
|
||||||
.color = color,
|
.color = color,
|
||||||
.target = target,
|
.target = target,
|
||||||
|
|
@ -368,6 +403,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
self.cells_bg.deinit();
|
self.cells_bg.deinit();
|
||||||
self.grayscale.deinit();
|
self.grayscale.deinit();
|
||||||
self.color.deinit();
|
self.color.deinit();
|
||||||
|
self.bg_image_buffer.deinit();
|
||||||
if (self.custom_shader_state) |*state| state.deinit();
|
if (self.custom_shader_state) |*state| state.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,6 +527,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
min_contrast: f32,
|
min_contrast: f32,
|
||||||
padding_color: configpkg.WindowPaddingColor,
|
padding_color: configpkg.WindowPaddingColor,
|
||||||
custom_shaders: configpkg.RepeatablePath,
|
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,
|
links: link.Set,
|
||||||
vsync: bool,
|
vsync: bool,
|
||||||
colorspace: configpkg.Config.WindowColorspace,
|
colorspace: configpkg.Config.WindowColorspace,
|
||||||
|
|
@ -507,6 +548,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
// Copy our shaders
|
// Copy our shaders
|
||||||
const custom_shaders = try config.@"custom-shader".clone(alloc);
|
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
|
// Copy our font features
|
||||||
const font_features = try config.@"font-feature".clone(alloc);
|
const font_features = try config.@"font-feature".clone(alloc);
|
||||||
|
|
||||||
|
|
@ -563,6 +611,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
null,
|
null,
|
||||||
|
|
||||||
.custom_shaders = custom_shaders,
|
.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,
|
.links = links,
|
||||||
.vsync = config.@"window-vsync",
|
.vsync = config.@"window-vsync",
|
||||||
.colorspace = config.@"window-colorspace",
|
.colorspace = config.@"window-colorspace",
|
||||||
|
|
@ -657,6 +710,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
.cell_size = undefined,
|
.cell_size = undefined,
|
||||||
.grid_size = undefined,
|
.grid_size = undefined,
|
||||||
.grid_padding = undefined,
|
.grid_padding = undefined,
|
||||||
|
.screen_size = undefined,
|
||||||
.padding_extend = .{},
|
.padding_extend = .{},
|
||||||
.min_contrast = options.config.min_contrast,
|
.min_contrast = options.config.min_contrast,
|
||||||
.cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
|
.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),
|
.previous_cursor_color = @splat(0),
|
||||||
.cursor_change_time = 0,
|
.cursor_change_time = 0,
|
||||||
},
|
},
|
||||||
|
.bg_image_buffer = undefined,
|
||||||
|
|
||||||
// Fonts
|
// Fonts
|
||||||
.font_grid = options.font_grid,
|
.font_grid = options.font_grid,
|
||||||
|
|
@ -711,6 +766,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
// Ensure our undefined values above are correctly initialized.
|
// Ensure our undefined values above are correctly initialized.
|
||||||
result.updateFontGridUniforms();
|
result.updateFontGridUniforms();
|
||||||
result.updateScreenSizeUniforms();
|
result.updateScreenSizeUniforms();
|
||||||
|
result.updateBgImageBuffer();
|
||||||
|
try result.prepBackgroundImage();
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -739,6 +796,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
}
|
}
|
||||||
self.image_placements.deinit(self.alloc);
|
self.image_placements.deinit(self.alloc);
|
||||||
|
|
||||||
|
if (self.bg_image) |img| img.deinit(self.alloc);
|
||||||
|
|
||||||
self.deinitShaders();
|
self.deinitShaders();
|
||||||
|
|
||||||
self.api.deinit();
|
self.api.deinit();
|
||||||
|
|
@ -1336,6 +1395,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
// Upload images to the GPU as necessary.
|
// Upload images to the GPU as necessary.
|
||||||
try self.uploadKittyImages();
|
try self.uploadKittyImages();
|
||||||
|
|
||||||
|
// Upload the background image to the GPU as necessary.
|
||||||
|
try self.uploadBackgroundImage();
|
||||||
|
|
||||||
// Update custom shader uniforms if necessary.
|
// Update custom shader uniforms if necessary.
|
||||||
try self.updateCustomShaderUniforms();
|
try self.updateCustomShaderUniforms();
|
||||||
|
|
||||||
|
|
@ -1344,6 +1406,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
try frame.cells_bg.sync(self.cells.bg_cells);
|
try frame.cells_bg.sync(self.cells.bg_cells);
|
||||||
const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
|
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
|
// If our font atlas changed, sync the texture data
|
||||||
texture: {
|
texture: {
|
||||||
const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
|
const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
|
||||||
|
|
@ -1376,18 +1445,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
}});
|
}});
|
||||||
defer pass.complete();
|
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
|
// NOTE: We don't use the clear_color for this because that
|
||||||
// would require us to do color space conversion on the
|
// would require us to do color space conversion on the
|
||||||
// CPU-side. In the future when we have utilities for
|
// CPU-side. In the future when we have utilities for
|
||||||
// that we should remove this step and use clear_color.
|
// that we should remove this step and use clear_color.
|
||||||
pass.step(.{
|
if (self.bg_image) |img| switch (img) {
|
||||||
.pipeline = self.shaders.pipelines.bg_color,
|
.ready => |texture| pass.step(.{
|
||||||
.uniforms = frame.uniforms.buffer,
|
.pipeline = self.shaders.pipelines.bg_image,
|
||||||
.buffers = &.{ null, frame.cells_bg.buffer },
|
.uniforms = frame.uniforms.buffer,
|
||||||
.draw = .{ .type = .triangle, .vertex_count = 3 },
|
.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
|
// Then we draw any kitty images that need
|
||||||
// to be behind text AND cell backgrounds.
|
// to be behind text AND cell backgrounds.
|
||||||
|
|
@ -1753,9 +1837,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
if (img_top_y > bot_y) return;
|
if (img_top_y > bot_y) return;
|
||||||
if (img_bot_y < top_y) return;
|
if (img_bot_y < top_y) return;
|
||||||
|
|
||||||
// We need to prep this image for upload if it isn't in the cache OR
|
// We need to prep this image for upload if it isn't in the
|
||||||
// it is in the cache but the transmit time doesn't match meaning this
|
// cache OR it is in the cache but the transmit time doesn't
|
||||||
// image is different.
|
// match meaning this image is different.
|
||||||
try self.prepKittyImage(image);
|
try self.prepKittyImage(image);
|
||||||
|
|
||||||
// Calculate the dimensions of our image, taking in to
|
// Calculate the dimensions of our image, taking in to
|
||||||
|
|
@ -1819,16 +1903,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
const pending: Image.Pending = .{
|
const pending: Image.Pending = .{
|
||||||
.width = image.width,
|
.width = image.width,
|
||||||
.height = image.height,
|
.height = image.height,
|
||||||
|
.pixel_format = switch (image.format) {
|
||||||
|
.gray => .gray,
|
||||||
|
.gray_alpha => .gray_alpha,
|
||||||
|
.rgb => .rgb,
|
||||||
|
.rgba => .rgba,
|
||||||
|
.png => unreachable, // should be decoded by now
|
||||||
|
},
|
||||||
.data = data.ptr,
|
.data = data.ptr,
|
||||||
};
|
};
|
||||||
|
|
||||||
const new_image: Image = switch (image.format) {
|
const new_image: Image = .{ .pending = pending };
|
||||||
.gray => .{ .pending_gray = pending },
|
|
||||||
.gray_alpha => .{ .pending_gray_alpha = pending },
|
|
||||||
.rgb => .{ .pending_rgb = pending },
|
|
||||||
.rgba => .{ .pending_rgba = pending },
|
|
||||||
.png => unreachable, // should be decoded by now
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!gop.found_existing) {
|
if (!gop.found_existing) {
|
||||||
gop.value_ptr.* = .{
|
gop.value_ptr.* = .{
|
||||||
|
|
@ -1842,6 +1927,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try gop.value_ptr.image.prepForUpload(self.alloc);
|
||||||
|
|
||||||
gop.value_ptr.transmit_time = image.transmit_time;
|
gop.value_ptr.transmit_time = image.transmit_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1850,27 +1937,109 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
fn uploadKittyImages(self: *Self) !void {
|
fn uploadKittyImages(self: *Self) !void {
|
||||||
var image_it = self.images.iterator();
|
var image_it = self.images.iterator();
|
||||||
while (image_it.next()) |kv| {
|
while (image_it.next()) |kv| {
|
||||||
switch (kv.value_ptr.image) {
|
const img = &kv.value_ptr.image;
|
||||||
.ready => {},
|
if (img.isUnloading()) {
|
||||||
|
img.deinit(self.alloc);
|
||||||
.pending_gray,
|
self.images.removeByPtr(kv.key_ptr);
|
||||||
.pending_gray_alpha,
|
return;
|
||||||
.pending_rgb,
|
|
||||||
.pending_rgba,
|
|
||||||
.replace_gray,
|
|
||||||
.replace_gray_alpha,
|
|
||||||
.replace_rgb,
|
|
||||||
.replace_rgba,
|
|
||||||
=> try kv.value_ptr.image.upload(self.alloc, &self.api),
|
|
||||||
|
|
||||||
.unload_pending,
|
|
||||||
.unload_replace,
|
|
||||||
.unload_ready,
|
|
||||||
=> {
|
|
||||||
kv.value_ptr.image.deinit(self.alloc);
|
|
||||||
self.images.removeByPtr(kv.key_ptr);
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
if (img.isPending()) try img.upload(self.alloc, &self.api);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1911,12 +2080,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
|
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
|
||||||
self.cursor_invert = config.cursor_invert;
|
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 old_blending = self.config.blending;
|
||||||
const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
|
const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
|
||||||
|
|
||||||
self.config.deinit();
|
self.config.deinit();
|
||||||
self.config = config.*;
|
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.
|
// Reset our viewport to force a rebuild, in case of a font change.
|
||||||
self.cells_viewport = null;
|
self.cells_viewport = null;
|
||||||
|
|
||||||
|
|
@ -1986,14 +2176,50 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||||
@floatFromInt(blank.bottom),
|
@floatFromInt(blank.bottom),
|
||||||
@floatFromInt(blank.left),
|
@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.
|
/// Update uniforms for the custom shaders, if necessary.
|
||||||
///
|
///
|
||||||
/// This should be called exactly once per frame, inside `drawFrame`.
|
/// This should be called exactly once per frame, inside `drawFrame`.
|
||||||
fn updateCustomShaderUniforms(
|
fn updateCustomShaderUniforms(self: *Self) !void {
|
||||||
self: *Self,
|
|
||||||
) !void {
|
|
||||||
// We only need to do this if we have custom shaders.
|
// We only need to do this if we have custom shaders.
|
||||||
if (!self.has_custom_shaders) return;
|
if (!self.has_custom_shaders) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,34 +40,27 @@ pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct {
|
||||||
transmit_time: std.time.Instant,
|
transmit_time: std.time.Instant,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The state for a single image that is to be rendered. The image can be
|
/// The state for a single image that is to be rendered.
|
||||||
/// pending upload or ready to use with a texture.
|
|
||||||
pub const Image = union(enum) {
|
pub const Image = union(enum) {
|
||||||
/// The image is pending upload to the GPU. The different keys are
|
/// The image data is pending upload to the GPU.
|
||||||
/// different formats since some formats aren't accepted by the GPU
|
|
||||||
/// and require conversion.
|
|
||||||
///
|
///
|
||||||
/// This data is owned by this union so it must be freed once the
|
/// This data is owned by this union so it must be freed once uploaded.
|
||||||
/// image is uploaded.
|
pending: Pending,
|
||||||
pending_gray: Pending,
|
|
||||||
pending_gray_alpha: Pending,
|
|
||||||
pending_rgb: Pending,
|
|
||||||
pending_rgba: Pending,
|
|
||||||
|
|
||||||
/// This is the same as the pending states but there is a texture
|
/// This is the same as the pending states but there is
|
||||||
/// already allocated that we want to replace.
|
/// a texture already allocated that we want to replace.
|
||||||
replace_gray: Replace,
|
replace: Replace,
|
||||||
replace_gray_alpha: Replace,
|
|
||||||
replace_rgb: Replace,
|
|
||||||
replace_rgba: Replace,
|
|
||||||
|
|
||||||
/// The image is uploaded and ready to be used.
|
/// The image is uploaded and ready to be used.
|
||||||
ready: Texture,
|
ready: Texture,
|
||||||
|
|
||||||
/// The image is uploaded but is scheduled to be unloaded.
|
/// The image isn't uploaded yet but is scheduled to be unloaded.
|
||||||
unload_pending: []u8,
|
unload_pending: Pending,
|
||||||
|
/// The image is uploaded and is scheduled to be unloaded.
|
||||||
unload_ready: Texture,
|
unload_ready: Texture,
|
||||||
unload_replace: struct { []u8, Texture },
|
/// The image is uploaded and scheduled to be replaced
|
||||||
|
/// with new data, but it's also scheduled to be unloaded.
|
||||||
|
unload_replace: Replace,
|
||||||
|
|
||||||
pub const Replace = struct {
|
pub const Replace = struct {
|
||||||
texture: Texture,
|
texture: Texture,
|
||||||
|
|
@ -78,53 +71,58 @@ pub const Image = union(enum) {
|
||||||
pub const Pending = struct {
|
pub const Pending = struct {
|
||||||
height: u32,
|
height: u32,
|
||||||
width: u32,
|
width: u32,
|
||||||
|
pixel_format: PixelFormat,
|
||||||
|
|
||||||
/// Data is always expected to be (width * height * depth). Depth
|
/// Data is always expected to be (width * height * bpp).
|
||||||
/// is based on the union key.
|
|
||||||
data: [*]u8,
|
data: [*]u8,
|
||||||
|
|
||||||
pub fn dataSlice(self: Pending, d: u32) []u8 {
|
pub fn dataSlice(self: Pending) []u8 {
|
||||||
return self.data[0..self.len(d)];
|
return self.data[0..self.len()];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn len(self: Pending, d: u32) u32 {
|
pub fn len(self: Pending) usize {
|
||||||
return self.width * self.height * d;
|
return self.width * self.height * self.pixel_format.bpp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const PixelFormat = enum {
|
||||||
|
/// 1 byte per pixel grayscale.
|
||||||
|
gray,
|
||||||
|
/// 2 bytes per pixel grayscale + alpha.
|
||||||
|
gray_alpha,
|
||||||
|
/// 3 bytes per pixel RGB.
|
||||||
|
rgb,
|
||||||
|
/// 3 bytes per pixel BGR.
|
||||||
|
bgr,
|
||||||
|
/// 4 byte per pixel RGBA.
|
||||||
|
rgba,
|
||||||
|
/// 4 byte per pixel BGRA.
|
||||||
|
bgra,
|
||||||
|
|
||||||
|
/// Get bytes per pixel for this format.
|
||||||
|
pub inline fn bpp(self: PixelFormat) usize {
|
||||||
|
return switch (self) {
|
||||||
|
.gray => 1,
|
||||||
|
.gray_alpha => 2,
|
||||||
|
.rgb => 3,
|
||||||
|
.bgr => 3,
|
||||||
|
.rgba => 4,
|
||||||
|
.bgra => 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn deinit(self: Image, alloc: Allocator) void {
|
pub fn deinit(self: Image, alloc: Allocator) void {
|
||||||
switch (self) {
|
switch (self) {
|
||||||
.pending_gray => |p| alloc.free(p.dataSlice(1)),
|
.pending,
|
||||||
.pending_gray_alpha => |p| alloc.free(p.dataSlice(2)),
|
.unload_pending,
|
||||||
.pending_rgb => |p| alloc.free(p.dataSlice(3)),
|
=> |p| alloc.free(p.dataSlice()),
|
||||||
.pending_rgba => |p| alloc.free(p.dataSlice(4)),
|
|
||||||
.unload_pending => |data| alloc.free(data),
|
|
||||||
|
|
||||||
.replace_gray => |r| {
|
.replace, .unload_replace => |r| {
|
||||||
alloc.free(r.pending.dataSlice(1));
|
alloc.free(r.pending.dataSlice());
|
||||||
r.texture.deinit();
|
r.texture.deinit();
|
||||||
},
|
},
|
||||||
|
|
||||||
.replace_gray_alpha => |r| {
|
|
||||||
alloc.free(r.pending.dataSlice(2));
|
|
||||||
r.texture.deinit();
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_rgb => |r| {
|
|
||||||
alloc.free(r.pending.dataSlice(3));
|
|
||||||
r.texture.deinit();
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_rgba => |r| {
|
|
||||||
alloc.free(r.pending.dataSlice(4));
|
|
||||||
r.texture.deinit();
|
|
||||||
},
|
|
||||||
|
|
||||||
.unload_replace => |r| {
|
|
||||||
alloc.free(r[0]);
|
|
||||||
r[1].deinit();
|
|
||||||
},
|
|
||||||
|
|
||||||
.ready,
|
.ready,
|
||||||
.unload_ready,
|
.unload_ready,
|
||||||
=> |t| t.deinit(),
|
=> |t| t.deinit(),
|
||||||
|
|
@ -139,150 +137,55 @@ pub const Image = union(enum) {
|
||||||
.unload_ready,
|
.unload_ready,
|
||||||
=> return,
|
=> return,
|
||||||
|
|
||||||
.ready => |obj| .{ .unload_ready = obj },
|
.ready => |t| .{ .unload_ready = t },
|
||||||
.pending_gray => |p| .{ .unload_pending = p.dataSlice(1) },
|
.pending => |p| .{ .unload_pending = p },
|
||||||
.pending_gray_alpha => |p| .{ .unload_pending = p.dataSlice(2) },
|
.replace => |r| .{ .unload_replace = r },
|
||||||
.pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) },
|
|
||||||
.pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) },
|
|
||||||
.replace_gray => |r| .{ .unload_replace = .{
|
|
||||||
r.pending.dataSlice(1), r.texture,
|
|
||||||
} },
|
|
||||||
.replace_gray_alpha => |r| .{ .unload_replace = .{
|
|
||||||
r.pending.dataSlice(2), r.texture,
|
|
||||||
} },
|
|
||||||
.replace_rgb => |r| .{ .unload_replace = .{
|
|
||||||
r.pending.dataSlice(3), r.texture,
|
|
||||||
} },
|
|
||||||
.replace_rgba => |r| .{ .unload_replace = .{
|
|
||||||
r.pending.dataSlice(4), r.texture,
|
|
||||||
} },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the currently pending image with a new one. This will
|
/// Mark the current image to be replaced with a pending one. This will
|
||||||
/// attempt to update the existing texture if it is already allocated.
|
/// attempt to update the existing texture if we have one, otherwise it
|
||||||
/// If the texture is not allocated, this will act like a new upload.
|
/// will act like a new upload.
|
||||||
///
|
|
||||||
/// This function only marks the image for replace. The actual logic
|
|
||||||
/// to replace is done later.
|
|
||||||
pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void {
|
pub fn markForReplace(self: *Image, alloc: Allocator, img: Image) !void {
|
||||||
assert(img.pending() != null);
|
assert(img.isPending());
|
||||||
|
|
||||||
// Get our existing texture. This switch statement will also handle
|
// If we have pending data right now, free it.
|
||||||
// scenarios where there is no existing texture and we can modify
|
if (self.getPending()) |p| {
|
||||||
// the self pointer directly.
|
alloc.free(p.dataSlice());
|
||||||
const existing: Texture = switch (self.*) {
|
}
|
||||||
// For pending, we can free the old
|
// If we have an existing texture, use it in the replace.
|
||||||
// data and become pending ourselves.
|
if (self.getTexture()) |t| {
|
||||||
.pending_gray => |p| {
|
self.* = .{ .replace = .{
|
||||||
alloc.free(p.dataSlice(1));
|
.texture = t,
|
||||||
self.* = img;
|
.pending = img.getPending().?,
|
||||||
return;
|
} };
|
||||||
},
|
return;
|
||||||
|
}
|
||||||
.pending_gray_alpha => |p| {
|
// Otherwise we just become a pending image.
|
||||||
alloc.free(p.dataSlice(2));
|
self.* = .{ .pending = img.getPending().? };
|
||||||
self.* = img;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
.pending_rgb => |p| {
|
|
||||||
alloc.free(p.dataSlice(3));
|
|
||||||
self.* = img;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
.pending_rgba => |p| {
|
|
||||||
alloc.free(p.dataSlice(4));
|
|
||||||
self.* = img;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
// If we're marked for unload but we just have pending data,
|
|
||||||
// this behaves the same as a normal "pending": free the data,
|
|
||||||
// become new pending.
|
|
||||||
.unload_pending => |data| {
|
|
||||||
alloc.free(data);
|
|
||||||
self.* = img;
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
.unload_replace => |r| existing: {
|
|
||||||
alloc.free(r[0]);
|
|
||||||
break :existing r[1];
|
|
||||||
},
|
|
||||||
|
|
||||||
// If we were already pending a replacement, then we free
|
|
||||||
// our existing pending data and use the same texture.
|
|
||||||
.replace_gray => |r| existing: {
|
|
||||||
alloc.free(r.pending.dataSlice(1));
|
|
||||||
break :existing r.texture;
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_gray_alpha => |r| existing: {
|
|
||||||
alloc.free(r.pending.dataSlice(2));
|
|
||||||
break :existing r.texture;
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_rgb => |r| existing: {
|
|
||||||
alloc.free(r.pending.dataSlice(3));
|
|
||||||
break :existing r.texture;
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_rgba => |r| existing: {
|
|
||||||
alloc.free(r.pending.dataSlice(4));
|
|
||||||
break :existing r.texture;
|
|
||||||
},
|
|
||||||
|
|
||||||
// For both ready and unload_ready, we need to replace
|
|
||||||
// the texture. We can't do that here, so we just mark
|
|
||||||
// ourselves for replacement.
|
|
||||||
.ready, .unload_ready => |tex| tex,
|
|
||||||
};
|
|
||||||
|
|
||||||
// We now have an existing texture, so set the proper replace key.
|
|
||||||
self.* = switch (img) {
|
|
||||||
.pending_gray => |p| .{ .replace_gray = .{
|
|
||||||
.texture = existing,
|
|
||||||
.pending = p,
|
|
||||||
} },
|
|
||||||
|
|
||||||
.pending_gray_alpha => |p| .{ .replace_gray_alpha = .{
|
|
||||||
.texture = existing,
|
|
||||||
.pending = p,
|
|
||||||
} },
|
|
||||||
|
|
||||||
.pending_rgb => |p| .{ .replace_rgb = .{
|
|
||||||
.texture = existing,
|
|
||||||
.pending = p,
|
|
||||||
} },
|
|
||||||
|
|
||||||
.pending_rgba => |p| .{ .replace_rgba = .{
|
|
||||||
.texture = existing,
|
|
||||||
.pending = p,
|
|
||||||
} },
|
|
||||||
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this image is pending upload.
|
/// Returns true if this image is pending upload.
|
||||||
pub fn isPending(self: Image) bool {
|
pub fn isPending(self: Image) bool {
|
||||||
return self.pending() != null;
|
return self.getPending() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this image is pending an unload.
|
/// Returns true if this image has an associated texture.
|
||||||
|
pub fn hasTexture(self: Image) bool {
|
||||||
|
return self.getTexture() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this image is marked for unload.
|
||||||
pub fn isUnloading(self: Image) bool {
|
pub fn isUnloading(self: Image) bool {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.unload_pending,
|
.unload_pending,
|
||||||
|
.unload_replace,
|
||||||
.unload_ready,
|
.unload_ready,
|
||||||
=> true,
|
=> true,
|
||||||
|
|
||||||
|
.pending,
|
||||||
|
.replace,
|
||||||
.ready,
|
.ready,
|
||||||
.pending_gray,
|
|
||||||
.pending_gray_alpha,
|
|
||||||
.pending_rgb,
|
|
||||||
.pending_rgba,
|
|
||||||
=> false,
|
=> false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -291,123 +194,109 @@ pub const Image = union(enum) {
|
||||||
/// If the data is already in a format that can be uploaded, this is a
|
/// If the data is already in a format that can be uploaded, this is a
|
||||||
/// no-op.
|
/// no-op.
|
||||||
pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
|
pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
|
||||||
|
const p = self.getPendingPointer().?;
|
||||||
// As things stand, we currently convert all images to RGBA before
|
// As things stand, we currently convert all images to RGBA before
|
||||||
// uploading to the GPU. This just makes things easier. In the future
|
// uploading to the GPU. This just makes things easier. In the future
|
||||||
// we may want to support other formats.
|
// we may want to support other formats.
|
||||||
switch (self.*) {
|
if (p.pixel_format == .rgba) return;
|
||||||
.ready,
|
// If the pending data isn't RGBA we'll need to swizzle it.
|
||||||
.unload_pending,
|
const data = p.dataSlice();
|
||||||
.unload_replace,
|
const rgba = try switch (p.pixel_format) {
|
||||||
.unload_ready,
|
.gray => wuffs.swizzle.gToRgba(alloc, data),
|
||||||
=> unreachable, // invalid
|
.gray_alpha => wuffs.swizzle.gaToRgba(alloc, data),
|
||||||
|
.rgb => wuffs.swizzle.rgbToRgba(alloc, data),
|
||||||
.pending_rgba,
|
.bgr => wuffs.swizzle.bgrToRgba(alloc, data),
|
||||||
.replace_rgba,
|
.rgba => unreachable,
|
||||||
=> {}, // ready
|
.bgra => wuffs.swizzle.bgraToRgba(alloc, data),
|
||||||
|
};
|
||||||
.pending_rgb => |*p| {
|
alloc.free(data);
|
||||||
const data = p.dataSlice(3);
|
p.data = rgba.ptr;
|
||||||
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
|
p.pixel_format = .rgba;
|
||||||
alloc.free(data);
|
|
||||||
p.data = rgba.ptr;
|
|
||||||
self.* = .{ .pending_rgba = p.* };
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_rgb => |*r| {
|
|
||||||
const data = r.pending.dataSlice(3);
|
|
||||||
const rgba = try wuffs.swizzle.rgbToRgba(alloc, data);
|
|
||||||
alloc.free(data);
|
|
||||||
r.pending.data = rgba.ptr;
|
|
||||||
self.* = .{ .replace_rgba = r.* };
|
|
||||||
},
|
|
||||||
|
|
||||||
.pending_gray => |*p| {
|
|
||||||
const data = p.dataSlice(1);
|
|
||||||
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
|
|
||||||
alloc.free(data);
|
|
||||||
p.data = rgba.ptr;
|
|
||||||
self.* = .{ .pending_rgba = p.* };
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_gray => |*r| {
|
|
||||||
const data = r.pending.dataSlice(2);
|
|
||||||
const rgba = try wuffs.swizzle.gToRgba(alloc, data);
|
|
||||||
alloc.free(data);
|
|
||||||
r.pending.data = rgba.ptr;
|
|
||||||
self.* = .{ .replace_rgba = r.* };
|
|
||||||
},
|
|
||||||
|
|
||||||
.pending_gray_alpha => |*p| {
|
|
||||||
const data = p.dataSlice(2);
|
|
||||||
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
|
|
||||||
alloc.free(data);
|
|
||||||
p.data = rgba.ptr;
|
|
||||||
self.* = .{ .pending_rgba = p.* };
|
|
||||||
},
|
|
||||||
|
|
||||||
.replace_gray_alpha => |*r| {
|
|
||||||
const data = r.pending.dataSlice(2);
|
|
||||||
const rgba = try wuffs.swizzle.gaToRgba(alloc, data);
|
|
||||||
alloc.free(data);
|
|
||||||
r.pending.data = rgba.ptr;
|
|
||||||
self.* = .{ .replace_rgba = r.* };
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload the pending image to the GPU and change the state of this
|
/// Prepare the pending image data for upload to the GPU.
|
||||||
/// image to ready.
|
/// This doesn't need GPU access so is safe to call any time.
|
||||||
|
pub fn prepForUpload(self: *Image, alloc: Allocator) !void {
|
||||||
|
assert(self.isPending());
|
||||||
|
|
||||||
|
try self.convert(alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload the pending image to the GPU and
|
||||||
|
/// change the state of this image to ready.
|
||||||
pub fn upload(
|
pub fn upload(
|
||||||
self: *Image,
|
self: *Image,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
api: *const GraphicsAPI,
|
api: *const GraphicsAPI,
|
||||||
) !void {
|
) !void {
|
||||||
// Convert our data if we have to
|
assert(self.isPending());
|
||||||
try self.convert(alloc);
|
|
||||||
|
try self.prepForUpload(alloc);
|
||||||
|
|
||||||
// Get our pending info
|
// Get our pending info
|
||||||
const p = self.pending().?;
|
const p = self.getPending().?;
|
||||||
|
|
||||||
// Create our texture
|
// Create our texture
|
||||||
const texture = try Texture.init(
|
const texture = try Texture.init(
|
||||||
api.imageTextureOptions(.rgba, true),
|
api.imageTextureOptions(.rgba, true),
|
||||||
@intCast(p.width),
|
@intCast(p.width),
|
||||||
@intCast(p.height),
|
@intCast(p.height),
|
||||||
p.data[0 .. p.width * p.height * self.depth()],
|
p.dataSlice(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Uploaded. We can now clear our data and change our state.
|
// Uploaded. We can now clear our data and change our state.
|
||||||
//
|
//
|
||||||
// NOTE: For "replace_*" states, this will free the old texture.
|
// NOTE: For the `replace` state, this will free the old texture.
|
||||||
// We don't currently actually replace the existing texture in-place
|
// We don't currently actually replace the existing texture
|
||||||
// but that is an optimization we can do later.
|
// in-place but that is an optimization we can do later.
|
||||||
self.deinit(alloc);
|
self.deinit(alloc);
|
||||||
self.* = .{ .ready = texture };
|
self.* = .{ .ready = texture };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Our pixel depth
|
/// Returns any pending image data for this image that requires upload.
|
||||||
fn depth(self: Image) u32 {
|
///
|
||||||
|
/// If there is no pending data to upload, returns null.
|
||||||
|
fn getPending(self: Image) ?Pending {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
.pending_rgb => 3,
|
.pending,
|
||||||
.pending_rgba => 4,
|
.unload_pending,
|
||||||
.replace_rgb => 3,
|
|
||||||
.replace_rgba => 4,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if this image is in a pending state and requires upload.
|
|
||||||
fn pending(self: Image) ?Pending {
|
|
||||||
return switch (self) {
|
|
||||||
.pending_rgb,
|
|
||||||
.pending_rgba,
|
|
||||||
=> |p| p,
|
=> |p| p,
|
||||||
|
|
||||||
.replace_rgb,
|
.replace,
|
||||||
.replace_rgba,
|
.unload_replace,
|
||||||
=> |r| r.pending,
|
=> |r| r.pending,
|
||||||
|
|
||||||
else => null,
|
else => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the texture for this image.
|
||||||
|
///
|
||||||
|
/// If there is no texture for it yet, returns null.
|
||||||
|
fn getTexture(self: Image) ?Texture {
|
||||||
|
return switch (self) {
|
||||||
|
.ready,
|
||||||
|
.unload_ready,
|
||||||
|
=> |t| t,
|
||||||
|
|
||||||
|
.replace,
|
||||||
|
.unload_replace,
|
||||||
|
=> |r| r.texture,
|
||||||
|
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as getPending but returns a pointer instead of a copy.
|
||||||
|
fn getPendingPointer(self: *Image) ?*Pending {
|
||||||
|
return switch (self.*) {
|
||||||
|
.pending => return &self.pending,
|
||||||
|
.unload_pending => return &self.unload_pending,
|
||||||
|
|
||||||
|
.replace => return &self.replace.pending,
|
||||||
|
.unload_replace => return &self.unload_replace.pending,
|
||||||
|
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ fn autoAttribute(T: type, attrs: objc.Object) void {
|
||||||
const offset = @offsetOf(T, field.name);
|
const offset = @offsetOf(T, field.name);
|
||||||
|
|
||||||
const FT = switch (@typeInfo(field.type)) {
|
const FT = switch (@typeInfo(field.type)) {
|
||||||
|
.@"struct" => |e| e.backing_integer.?,
|
||||||
.@"enum" => |e| e.tag_type,
|
.@"enum" => |e| e.tag_type,
|
||||||
else => field.type,
|
else => field.type,
|
||||||
};
|
};
|
||||||
|
|
@ -169,13 +170,17 @@ fn autoAttribute(T: type, attrs: objc.Object) void {
|
||||||
[4]u8 => mtl.MTLVertexFormat.uchar4,
|
[4]u8 => mtl.MTLVertexFormat.uchar4,
|
||||||
[2]u16 => mtl.MTLVertexFormat.ushort2,
|
[2]u16 => mtl.MTLVertexFormat.ushort2,
|
||||||
[2]i16 => mtl.MTLVertexFormat.short2,
|
[2]i16 => mtl.MTLVertexFormat.short2,
|
||||||
|
f32 => mtl.MTLVertexFormat.float,
|
||||||
[2]f32 => mtl.MTLVertexFormat.float2,
|
[2]f32 => mtl.MTLVertexFormat.float2,
|
||||||
[4]f32 => mtl.MTLVertexFormat.float4,
|
[4]f32 => mtl.MTLVertexFormat.float4,
|
||||||
|
i32 => mtl.MTLVertexFormat.int,
|
||||||
[2]i32 => mtl.MTLVertexFormat.int2,
|
[2]i32 => mtl.MTLVertexFormat.int2,
|
||||||
|
[4]i32 => mtl.MTLVertexFormat.int2,
|
||||||
u32 => mtl.MTLVertexFormat.uint,
|
u32 => mtl.MTLVertexFormat.uint,
|
||||||
[2]u32 => mtl.MTLVertexFormat.uint2,
|
[2]u32 => mtl.MTLVertexFormat.uint2,
|
||||||
[4]u32 => mtl.MTLVertexFormat.uint4,
|
[4]u32 => mtl.MTLVertexFormat.uint4,
|
||||||
u8 => mtl.MTLVertexFormat.uchar,
|
u8 => mtl.MTLVertexFormat.uchar,
|
||||||
|
i8 => mtl.MTLVertexFormat.char,
|
||||||
else => comptime unreachable,
|
else => comptime unreachable,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,13 @@ const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
|
||||||
.step_fn = .per_instance,
|
.step_fn = .per_instance,
|
||||||
.blending_enabled = true,
|
.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
|
/// 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.
|
/// This is calculated based on the size of the screen.
|
||||||
projection_matrix: math.Mat align(16),
|
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.
|
/// Size of a single cell in pixels, unscaled.
|
||||||
cell_size: [2]f32 align(8),
|
cell_size: [2]f32 align(8),
|
||||||
|
|
||||||
|
|
@ -288,6 +298,38 @@ pub const Image = extern struct {
|
||||||
dest_size: [2]f32,
|
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.
|
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
|
||||||
fn initLibrary(device: objc.Object) !objc.Object {
|
fn initLibrary(device: objc.Object) !objc.Object {
|
||||||
const start = try std.time.Instant.now();
|
const start = try std.time.Instant.now();
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ fn autoAttribute(
|
||||||
const offset = @offsetOf(T, field.name);
|
const offset = @offsetOf(T, field.name);
|
||||||
|
|
||||||
const FT = switch (@typeInfo(field.type)) {
|
const FT = switch (@typeInfo(field.type)) {
|
||||||
|
.@"struct" => |s| s.backing_integer.?,
|
||||||
.@"enum" => |e| e.tag_type,
|
.@"enum" => |e| e.tag_type,
|
||||||
else => field.type,
|
else => field.type,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ pub fn init(
|
||||||
opts.internal_format,
|
opts.internal_format,
|
||||||
@intCast(width),
|
@intCast(width),
|
||||||
@intCast(height),
|
@intCast(height),
|
||||||
0,
|
|
||||||
opts.format,
|
opts.format,
|
||||||
.UnsignedByte,
|
.UnsignedByte,
|
||||||
if (data) |d| @ptrCast(d.ptr) else null,
|
if (data) |d| @ptrCast(d.ptr) else null,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,13 @@ const pipeline_descs: []const struct { [:0]const u8, PipelineDescription } =
|
||||||
.step_fn = .per_instance,
|
.step_fn = .per_instance,
|
||||||
.blending_enabled = true,
|
.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
|
/// 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.
|
/// This is calculated based on the size of the screen.
|
||||||
projection_matrix: math.Mat align(16),
|
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.
|
/// Size of a single cell in pixels, unscaled.
|
||||||
cell_size: [2]f32 align(8),
|
cell_size: [2]f32 align(8),
|
||||||
|
|
||||||
|
|
@ -256,6 +266,38 @@ pub const Image = extern struct {
|
||||||
dest_size: [2]f32 align(8),
|
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
|
/// Initialize our custom shader pipelines. The shaders argument is a
|
||||||
/// set of shader source code, not file paths.
|
/// set of shader source code, not file paths.
|
||||||
fn initPostPipelines(
|
fn initPostPipelines(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
//----------------------------------------------------------------------------//
|
//----------------------------------------------------------------------------//
|
||||||
layout(binding = 1, std140) uniform Globals {
|
layout(binding = 1, std140) uniform Globals {
|
||||||
uniform mat4 projection_matrix;
|
uniform mat4 projection_matrix;
|
||||||
|
uniform vec2 screen_size;
|
||||||
uniform vec2 cell_size;
|
uniform vec2 cell_size;
|
||||||
uniform uint grid_size_packed_2u16;
|
uniform uint grid_size_packed_2u16;
|
||||||
uniform vec4 grid_padding;
|
uniform vec4 grid_padding;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ enum Padding : uint8_t {
|
||||||
|
|
||||||
struct Uniforms {
|
struct Uniforms {
|
||||||
float4x4 projection_matrix;
|
float4x4 projection_matrix;
|
||||||
|
float2 screen_size;
|
||||||
float2 cell_size;
|
float2 cell_size;
|
||||||
ushort2 grid_size;
|
ushort2 grid_size;
|
||||||
float4 grid_padding;
|
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
|
// Cell Background Shader
|
||||||
//-------------------------------------------------------------------
|
//-------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue