diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 4a1d61433..2c8e05eff 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -74,6 +74,9 @@ pub const InternalFormat = enum(c_int) { srgb = c.GL_SRGB8, 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. _, }; @@ -126,7 +129,6 @@ pub const Binding = struct { internal_format: InternalFormat, width: c.GLsizei, height: c.GLsizei, - border: c.GLint, format: Format, typ: DataType, data: ?*const anyopaque, @@ -137,7 +139,7 @@ pub const Binding = struct { @intFromEnum(internal_format), width, height, - border, + 0, @intFromEnum(format), @intFromEnum(typ), data, diff --git a/pkg/wuffs/src/main.zig b/pkg/wuffs/src/main.zig index be4eb9184..89f3c008c 100644 --- a/pkg/wuffs/src/main.zig +++ b/pkg/wuffs/src/main.zig @@ -8,7 +8,7 @@ pub const Error = @import("error.zig").Error; pub const ImageData = struct { width: u32, height: u32, - data: []const u8, + data: []u8, }; test { diff --git a/pkg/wuffs/src/swizzle.zig b/pkg/wuffs/src/swizzle.zig index d57da98a9..352cf2b50 100644 --- a/pkg/wuffs/src/swizzle.zig +++ b/pkg/wuffs/src/swizzle.zig @@ -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( alloc: Allocator, src: []const u8, diff --git a/src/config.zig b/src/config.zig index 018d0e6e8..7f390fb08 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 7905d00ec..ebc54c3b4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.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. 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 selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). @@ -3298,6 +3385,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 +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 pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults diff --git a/src/file_type.zig b/src/file_type.zig new file mode 100644 index 000000000..18dd7a4a5 --- /dev/null +++ b/src/file_type.zig @@ -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; + } +}; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 39b6f7efc..3899bb8c5 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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 { diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d254934e4..e112c0df7 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -388,13 +388,14 @@ 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 { _ = self; return .{ .format = .rgba, - .internal_format = .srgba, + .internal_format = .srgba_compressed, .target = .@"2D", }; } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 9589cb44b..bf189fc4c 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -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. - pass.step(.{ - .pipeline = self.shaders.pipelines.bg_color, - .uniforms = frame.uniforms.buffer, - .buffers = &.{ null, frame.cells_bg.buffer }, - .draw = .{ .type = .triangle, .vertex_count = 3 }, - }); + 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. @@ -1753,9 +1837,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { if (img_top_y > bot_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 - // it is in the cache but the transmit time doesn't match meaning this - // image is different. + // We need to prep this image for upload if it isn't in the + // cache OR it is in the cache but the transmit time doesn't + // match meaning this image is different. try self.prepKittyImage(image); // Calculate the dimensions of our image, taking in to @@ -1819,16 +1903,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const pending: Image.Pending = .{ .width = image.width, .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, }; - const new_image: Image = switch (image.format) { - .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 - }; + const new_image: Image = .{ .pending = pending }; if (!gop.found_existing) { 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; } @@ -1850,27 +1937,109 @@ pub fn Renderer(comptime GraphicsAPI: type) type { fn uploadKittyImages(self: *Self) !void { var image_it = self.images.iterator(); while (image_it.next()) |kv| { - switch (kv.value_ptr.image) { - .ready => {}, - - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, - .replace_gray, - .replace_gray_alpha, - .replace_rgb, - .replace_rgba, - => try kv.value_ptr.image.upload(self.alloc, &self.api), - - .unload_pending, - .unload_replace, - .unload_ready, - => { - kv.value_ptr.image.deinit(self.alloc); - self.images.removeByPtr(kv.key_ptr); - }, + const img = &kv.value_ptr.image; + if (img.isUnloading()) { + img.deinit(self.alloc); + self.images.removeByPtr(kv.key_ptr); + return; } + 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.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; @@ -1986,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; diff --git a/src/renderer/image.zig b/src/renderer/image.zig index 277ddd8c0..d89c46730 100644 --- a/src/renderer/image.zig +++ b/src/renderer/image.zig @@ -40,34 +40,27 @@ pub const ImageMap = std.AutoHashMapUnmanaged(u32, struct { transmit_time: std.time.Instant, }); -/// The state for a single image that is to be rendered. The image can be -/// pending upload or ready to use with a texture. +/// The state for a single image that is to be rendered. pub const Image = union(enum) { - /// The image is pending upload to the GPU. The different keys are - /// different formats since some formats aren't accepted by the GPU - /// and require conversion. + /// The image data is pending upload to the GPU. /// - /// This data is owned by this union so it must be freed once the - /// image is uploaded. - pending_gray: Pending, - pending_gray_alpha: Pending, - pending_rgb: Pending, - pending_rgba: Pending, + /// This data is owned by this union so it must be freed once uploaded. + pending: Pending, - /// This is the same as the pending states but there is a texture - /// already allocated that we want to replace. - replace_gray: Replace, - replace_gray_alpha: Replace, - replace_rgb: Replace, - replace_rgba: Replace, + /// This is the same as the pending states but there is + /// a texture already allocated that we want to replace. + replace: Replace, /// The image is uploaded and ready to be used. ready: Texture, - /// The image is uploaded but is scheduled to be unloaded. - unload_pending: []u8, + /// The image isn't uploaded yet but is scheduled to be unloaded. + unload_pending: Pending, + /// The image is uploaded and is scheduled to be unloaded. 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 { texture: Texture, @@ -78,53 +71,58 @@ pub const Image = union(enum) { pub const Pending = struct { height: u32, width: u32, + pixel_format: PixelFormat, - /// Data is always expected to be (width * height * depth). Depth - /// is based on the union key. + /// Data is always expected to be (width * height * bpp). data: [*]u8, - pub fn dataSlice(self: Pending, d: u32) []u8 { - return self.data[0..self.len(d)]; + pub fn dataSlice(self: Pending) []u8 { + return self.data[0..self.len()]; } - pub fn len(self: Pending, d: u32) u32 { - return self.width * self.height * d; + pub fn len(self: Pending) usize { + 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 { switch (self) { - .pending_gray => |p| alloc.free(p.dataSlice(1)), - .pending_gray_alpha => |p| alloc.free(p.dataSlice(2)), - .pending_rgb => |p| alloc.free(p.dataSlice(3)), - .pending_rgba => |p| alloc.free(p.dataSlice(4)), - .unload_pending => |data| alloc.free(data), + .pending, + .unload_pending, + => |p| alloc.free(p.dataSlice()), - .replace_gray => |r| { - alloc.free(r.pending.dataSlice(1)); + .replace, .unload_replace => |r| { + alloc.free(r.pending.dataSlice()); r.texture.deinit(); }, - .replace_gray_alpha => |r| { - alloc.free(r.pending.dataSlice(2)); - r.texture.deinit(); - }, - - .replace_rgb => |r| { - alloc.free(r.pending.dataSlice(3)); - r.texture.deinit(); - }, - - .replace_rgba => |r| { - alloc.free(r.pending.dataSlice(4)); - r.texture.deinit(); - }, - - .unload_replace => |r| { - alloc.free(r[0]); - r[1].deinit(); - }, - .ready, .unload_ready, => |t| t.deinit(), @@ -139,150 +137,55 @@ pub const Image = union(enum) { .unload_ready, => return, - .ready => |obj| .{ .unload_ready = obj }, - .pending_gray => |p| .{ .unload_pending = p.dataSlice(1) }, - .pending_gray_alpha => |p| .{ .unload_pending = p.dataSlice(2) }, - .pending_rgb => |p| .{ .unload_pending = p.dataSlice(3) }, - .pending_rgba => |p| .{ .unload_pending = p.dataSlice(4) }, - .replace_gray => |r| .{ .unload_replace = .{ - r.pending.dataSlice(1), r.texture, - } }, - .replace_gray_alpha => |r| .{ .unload_replace = .{ - r.pending.dataSlice(2), r.texture, - } }, - .replace_rgb => |r| .{ .unload_replace = .{ - r.pending.dataSlice(3), r.texture, - } }, - .replace_rgba => |r| .{ .unload_replace = .{ - r.pending.dataSlice(4), r.texture, - } }, + .ready => |t| .{ .unload_ready = t }, + .pending => |p| .{ .unload_pending = p }, + .replace => |r| .{ .unload_replace = r }, }; } - /// Replace the currently pending image with a new one. This will - /// attempt to update the existing texture if it is already allocated. - /// If the texture is not allocated, this will act like a new upload. - /// - /// This function only marks the image for replace. The actual logic - /// to replace is done later. + /// Mark the current image to be replaced with a pending one. This will + /// attempt to update the existing texture if we have one, otherwise it + /// will act like a new upload. pub fn 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 - // scenarios where there is no existing texture and we can modify - // the self pointer directly. - const existing: Texture = switch (self.*) { - // For pending, we can free the old - // data and become pending ourselves. - .pending_gray => |p| { - alloc.free(p.dataSlice(1)); - self.* = img; - return; - }, - - .pending_gray_alpha => |p| { - alloc.free(p.dataSlice(2)); - self.* = img; - return; - }, - - .pending_rgb => |p| { - alloc.free(p.dataSlice(3)); - self.* = img; - return; - }, - - .pending_rgba => |p| { - alloc.free(p.dataSlice(4)); - self.* = img; - return; - }, - - // If we're marked for unload but we just have pending data, - // this behaves the same as a normal "pending": free the data, - // become new pending. - .unload_pending => |data| { - alloc.free(data); - self.* = img; - return; - }, - - .unload_replace => |r| existing: { - alloc.free(r[0]); - break :existing r[1]; - }, - - // If we were already pending a replacement, then we free - // our existing pending data and use the same texture. - .replace_gray => |r| existing: { - alloc.free(r.pending.dataSlice(1)); - break :existing r.texture; - }, - - .replace_gray_alpha => |r| existing: { - alloc.free(r.pending.dataSlice(2)); - break :existing r.texture; - }, - - .replace_rgb => |r| existing: { - alloc.free(r.pending.dataSlice(3)); - break :existing r.texture; - }, - - .replace_rgba => |r| existing: { - alloc.free(r.pending.dataSlice(4)); - break :existing r.texture; - }, - - // For both ready and unload_ready, we need to replace - // the texture. We can't do that here, so we just mark - // ourselves for replacement. - .ready, .unload_ready => |tex| tex, - }; - - // We now have an existing texture, so set the proper replace key. - self.* = switch (img) { - .pending_gray => |p| .{ .replace_gray = .{ - .texture = existing, - .pending = p, - } }, - - .pending_gray_alpha => |p| .{ .replace_gray_alpha = .{ - .texture = existing, - .pending = p, - } }, - - .pending_rgb => |p| .{ .replace_rgb = .{ - .texture = existing, - .pending = p, - } }, - - .pending_rgba => |p| .{ .replace_rgba = .{ - .texture = existing, - .pending = p, - } }, - - else => unreachable, - }; + // If we have pending data right now, free it. + if (self.getPending()) |p| { + alloc.free(p.dataSlice()); + } + // If we have an existing texture, use it in the replace. + if (self.getTexture()) |t| { + self.* = .{ .replace = .{ + .texture = t, + .pending = img.getPending().?, + } }; + return; + } + // Otherwise we just become a pending image. + self.* = .{ .pending = img.getPending().? }; } /// Returns true if this image is pending upload. 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 { return switch (self) { .unload_pending, + .unload_replace, .unload_ready, => true, + .pending, + .replace, .ready, - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, => 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 /// no-op. 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 // uploading to the GPU. This just makes things easier. In the future // we may want to support other formats. - switch (self.*) { - .ready, - .unload_pending, - .unload_replace, - .unload_ready, - => unreachable, // invalid - - .pending_rgba, - .replace_rgba, - => {}, // ready - - .pending_rgb => |*p| { - const data = p.dataSlice(3); - const rgba = try wuffs.swizzle.rgbToRgba(alloc, data); - alloc.free(data); - p.data = rgba.ptr; - self.* = .{ .pending_rgba = p.* }; - }, - - .replace_rgb => |*r| { - const data = r.pending.dataSlice(3); - const rgba = try wuffs.swizzle.rgbToRgba(alloc, data); - alloc.free(data); - r.pending.data = rgba.ptr; - self.* = .{ .replace_rgba = r.* }; - }, - - .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.* }; - }, - } + if (p.pixel_format == .rgba) return; + // If the pending data isn't RGBA we'll need to swizzle it. + const data = p.dataSlice(); + const rgba = try switch (p.pixel_format) { + .gray => wuffs.swizzle.gToRgba(alloc, data), + .gray_alpha => wuffs.swizzle.gaToRgba(alloc, data), + .rgb => wuffs.swizzle.rgbToRgba(alloc, data), + .bgr => wuffs.swizzle.bgrToRgba(alloc, data), + .rgba => unreachable, + .bgra => wuffs.swizzle.bgraToRgba(alloc, data), + }; + alloc.free(data); + p.data = rgba.ptr; + p.pixel_format = .rgba; } - /// Upload the pending image to the GPU and change the state of this - /// image to ready. + /// Prepare the pending image data for upload to the GPU. + /// 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( self: *Image, alloc: Allocator, api: *const GraphicsAPI, ) !void { - // Convert our data if we have to - try self.convert(alloc); + assert(self.isPending()); + + try self.prepForUpload(alloc); // Get our pending info - const p = self.pending().?; + const p = self.getPending().?; // Create our texture const texture = try Texture.init( api.imageTextureOptions(.rgba, true), @intCast(p.width), @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. // - // NOTE: For "replace_*" states, this will free the old texture. - // We don't currently actually replace the existing texture in-place - // but that is an optimization we can do later. + // NOTE: For the `replace` state, this will free the old texture. + // We don't currently actually replace the existing texture + // in-place but that is an optimization we can do later. self.deinit(alloc); self.* = .{ .ready = texture }; } - /// Our pixel depth - fn depth(self: Image) u32 { + /// Returns any pending image data for this image that requires upload. + /// + /// If there is no pending data to upload, returns null. + fn getPending(self: Image) ?Pending { return switch (self) { - .pending_rgb => 3, - .pending_rgba => 4, - .replace_rgb => 3, - .replace_rgba => 4, - else => unreachable, - }; - } - - /// Returns true if this image is in a pending state and requires upload. - fn pending(self: Image) ?Pending { - return switch (self) { - .pending_rgb, - .pending_rgba, + .pending, + .unload_pending, => |p| p, - .replace_rgb, - .replace_rgba, + .replace, + .unload_replace, => |r| r.pending, 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, + }; + } }; diff --git a/src/renderer/metal/Pipeline.zig b/src/renderer/metal/Pipeline.zig index f72aeb2e1..0b8e99159 100644 --- a/src/renderer/metal/Pipeline.zig +++ b/src/renderer/metal/Pipeline.zig @@ -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, }; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 59a3a1a37..9fe0862ed 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -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(); diff --git a/src/renderer/opengl/Pipeline.zig b/src/renderer/opengl/Pipeline.zig index 501e6124c..c3d414ff2 100644 --- a/src/renderer/opengl/Pipeline.zig +++ b/src/renderer/opengl/Pipeline.zig @@ -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, }; diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 07123922f..9be2b7078 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -57,7 +57,6 @@ pub fn init( opts.internal_format, @intCast(width), @intCast(height), - 0, opts.format, .UnsignedByte, if (data) |d| @ptrCast(d.ptr) else null, diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig index cc7a3ea2e..0b67eaff0 100644 --- a/src/renderer/opengl/shaders.zig +++ b/src/renderer/opengl/shaders.zig @@ -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( diff --git a/src/renderer/shaders/glsl/bg_image.f.glsl b/src/renderer/shaders/glsl/bg_image.f.glsl new file mode 100644 index 000000000..7c3e4363a --- /dev/null +++ b/src/renderer/shaders/glsl/bg_image.f.glsl @@ -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; +} diff --git a/src/renderer/shaders/glsl/bg_image.v.glsl b/src/renderer/shaders/glsl/bg_image.v.glsl new file mode 100644 index 000000000..875c40518 --- /dev/null +++ b/src/renderer/shaders/glsl/bg_image.v.glsl @@ -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); +} diff --git a/src/renderer/shaders/glsl/common.glsl b/src/renderer/shaders/glsl/common.glsl index 0450d0c06..a0ed9f7b4 100644 --- a/src/renderer/shaders/glsl/common.glsl +++ b/src/renderer/shaders/glsl/common.glsl @@ -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; diff --git a/src/renderer/shaders/shaders.metal b/src/renderer/shaders/shaders.metal index 19652d836..b62e0c3cf 100644 --- a/src/renderer/shaders/shaders.metal +++ b/src/renderer/shaders/shaders.metal @@ -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 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 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 //-------------------------------------------------------------------