From da46a47726f39b5ca564d49ec397d76d4f1725ff Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 25 Jun 2025 09:28:51 -0600 Subject: [PATCH] renderer: add support for background images Adds support for background images via the `background-image` config. Resolves #3645, supersedes PRs #4226 and #5233. See docs of added config keys for usage details. --- src/config.zig | 3 + src/config/Config.zig | 109 +++++++++ src/renderer/Metal.zig | 1 + src/renderer/OpenGL.zig | 1 + src/renderer/generic.zig | 257 +++++++++++++++++++++- src/renderer/metal/Pipeline.zig | 5 + src/renderer/metal/shaders.zig | 42 ++++ src/renderer/opengl/Pipeline.zig | 1 + src/renderer/opengl/shaders.zig | 42 ++++ src/renderer/shaders/glsl/bg_image.f.glsl | 62 ++++++ src/renderer/shaders/glsl/bg_image.v.glsl | 145 ++++++++++++ src/renderer/shaders/glsl/common.glsl | 1 + src/renderer/shaders/shaders.metal | 212 ++++++++++++++++++ 13 files changed, 871 insertions(+), 10 deletions(-) create mode 100644 src/renderer/shaders/glsl/bg_image.f.glsl create mode 100644 src/renderer/shaders/glsl/bg_image.v.glsl 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..e1cccef1b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -466,6 +466,84 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, +/// Background image for the terminal. +/// +/// This should be a path to a PNG or JPEG file, +/// other image formats are not yet supported. +@"background-image": ?Path = null, + +/// Background image opacity. +/// +/// This is relative to the value of `background-opacity`. +/// +/// A value of `1.0` (the default) will result in the background image being +/// placed on top of the general background color, and then the combined result +/// will be adjusted to the opacity specified by `background-opacity`. +/// +/// A value less than `1.0` will result in the background image being mixed +/// with the general background color before the combined result is adjusted +/// to the configured `background-opacity`. +/// +/// A value greater than `1.0` will result in the background image having a +/// higher opacity than the general background color. For instance, if the +/// configured `background-opacity` is `0.5` and `background-image-opacity` +/// is set to `1.5`, then the final opacity of the background image will be +/// `0.5 * 1.5 = 0.75`. +@"background-image-opacity": f32 = 1.0, + +/// Background image position. +/// +/// Valid values are: +/// * `top-left` +/// * `top-center` +/// * `top-right` +/// * `center-left` +/// * `center` +/// * `center-right` +/// * `bottom-left` +/// * `bottom-center` +/// * `bottom-right` +/// +/// The default value is `center`. +@"background-image-position": BackgroundImagePosition = .center, + +/// Background image fit. +/// +/// Valid values are: +/// +/// * `contain` +/// +/// Preserving the aspect ratio, scale the background image to the largest +/// size that can still be contained within the terminal, so that the whole +/// image is visible. +/// +/// * `cover` +/// +/// Preserving the aspect ratio, scale the background image to the smallest +/// size that can completely cover the terminal. This may result in one or +/// more edges of the image being clipped by the edge of the terminal. +/// +/// * `stretch` +/// +/// Stretch the background image to the full size of the terminal, without +/// preserving the aspect ratio. +/// +/// * `none` +/// +/// Don't scale the background image. +/// +/// The default value is `contain`. +@"background-image-fit": BackgroundImageFit = .contain, + +/// Whether to repeat the background image or not. +/// +/// If this is set to true, the background image will be repeated if there +/// would otherwise be blank space around it because it doesn't completely +/// fill the terminal area. +/// +/// The default value is `false`. +@"background-image-repeat": bool = false, + /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). @@ -3298,6 +3376,15 @@ fn expandPaths(self: *Config, base: []const u8) !void { &self._diagnostics, ); }, + ?RepeatablePath, ?Path => { + if (@field(self, field.name)) |*path| { + try path.expand( + arena_alloc, + base, + &self._diagnostics, + ); + } + }, else => {}, } } @@ -6569,6 +6656,28 @@ pub const AlphaBlending = enum { } }; +/// See background-image-position +pub const BackgroundImagePosition = enum { + @"top-left", + @"top-center", + @"top-right", + @"center-left", + @"center-center", + @"center-right", + @"bottom-left", + @"bottom-center", + @"bottom-right", + center, +}; + +/// See background-image-fit +pub const BackgroundImageFit = enum { + contain, + cover, + stretch, + none, +}; + /// See freetype-load-flag pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults 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..3b4ba6d80 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -388,6 +388,7 @@ pub const uniformBufferOptions = bufferOptions; pub const fgBufferOptions = bufferOptions; pub const bgBufferOptions = bufferOptions; pub const imageBufferOptions = bufferOptions; +pub const bgImageBufferOptions = bufferOptions; /// Returns the options to use when constructing textures. pub inline fn textureOptions(self: OpenGL) Texture.Options { diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 194c7f910..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. @@ -1863,6 +1947,102 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } } + /// Call this any time the background image path changes. + /// + /// Caller must hold the draw mutex. + fn prepBackgroundImage(self: *Self) !void { + // Then we try to load the background image if we have a path. + if (self.config.bg_image) |p| load_background: { + const path = switch (p) { + .required, .optional => |slice| slice, + }; + + // Open the file + var file = std.fs.openFileAbsolute(path, .{}) catch |err| { + log.warn( + "error opening background image file \"{s}\": {}", + .{ path, err }, + ); + break :load_background; + }; + defer file.close(); + + // Read it + const contents = file.readToEndAlloc( + self.alloc, + std.math.maxInt(u32), // Max size of 4 GiB, for now. + ) catch |err| { + log.warn( + "error reading background image file \"{s}\": {}", + .{ path, err }, + ); + break :load_background; + }; + defer self.alloc.free(contents); + + // Figure out what type it probably is. + const file_type = switch (FileType.detect(contents)) { + .unknown => FileType.guessFromExtension( + std.fs.path.extension(path), + ), + else => |t| t, + }; + + // Decode it if we know how. + const image_data = switch (file_type) { + .png => try wuffs.png.decode(self.alloc, contents), + .jpeg => try wuffs.jpeg.decode(self.alloc, contents), + .unknown => { + log.warn( + "Cannot determine file type for background image file \"{s}\"!", + .{path}, + ); + break :load_background; + }, + else => |f| { + log.warn( + "Unsupported file type {} for background image file \"{s}\"!", + .{ f, path }, + ); + break :load_background; + }, + }; + + const image: imagepkg.Image = .{ + .pending = .{ + .width = image_data.width, + .height = image_data.height, + .pixel_format = .rgba, + .data = image_data.data.ptr, + }, + }; + + // If we have an existing background image, replace it. + // Otherwise, set this as our background image directly. + if (self.bg_image) |*img| { + try img.markForReplace(self.alloc, image); + } else { + self.bg_image = image; + } + } else { + // If we don't have a background image path, mark our + // background image for unload if we currently have one. + if (self.bg_image) |*img| img.markForUnload(); + } + } + + fn uploadBackgroundImage(self: *Self) !void { + // Make sure our bg image is uploaded if it needs to be. + if (self.bg_image) |*bg| { + if (bg.isUnloading()) { + bg.deinit(self.alloc); + self.bg_image = null; + return; + } + if (bg.isPending()) try bg.upload(self.alloc, &self.api); + } + } + /// Update the configuration. pub fn changeConfig(self: *Self, config: *DerivedConfig) !void { self.draw_mutex.lock(); @@ -1900,12 +2080,33 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; self.cursor_invert = config.cursor_invert; + const bg_image_config_changed = + self.config.bg_image_fit != config.bg_image_fit or + self.config.bg_image_position != config.bg_image_position or + self.config.bg_image_repeat != config.bg_image_repeat or + self.config.bg_image_opacity != config.bg_image_opacity; + + const bg_image_changed = + if (self.config.bg_image) |old| + if (config.bg_image) |new| + !old.equal(new) + else + true + else + config.bg_image != null; + const old_blending = self.config.blending; const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders); self.config.deinit(); self.config = config.*; + // If our background image path changed, prepare the new bg image. + if (bg_image_changed) try self.prepBackgroundImage(); + + // If our background image config changed, update the vertex buffer. + if (bg_image_config_changed) self.updateBgImageBuffer(); + // Reset our viewport to force a rebuild, in case of a font change. self.cells_viewport = null; @@ -1975,14 +2176,50 @@ pub fn Renderer(comptime GraphicsAPI: type) type { @floatFromInt(blank.bottom), @floatFromInt(blank.left), }; + self.uniforms.screen_size = .{ + @floatFromInt(self.size.screen.width), + @floatFromInt(self.size.screen.height), + }; + } + + /// Update the background image vertex buffer (CPU-side). + /// + /// This should be called if and when configs change that + /// could affect the background image. + /// + /// Caller must hold the draw mutex. + fn updateBgImageBuffer(self: *Self) void { + self.bg_image_buffer = .{ + .opacity = self.config.bg_image_opacity, + .info = .{ + .position = switch (self.config.bg_image_position) { + .@"top-left" => .tl, + .@"top-center" => .tc, + .@"top-right" => .tr, + .@"center-left" => .ml, + .@"center-center", .center => .mc, + .@"center-right" => .mr, + .@"bottom-left" => .bl, + .@"bottom-center" => .bc, + .@"bottom-right" => .br, + }, + .fit = switch (self.config.bg_image_fit) { + .contain => .contain, + .cover => .cover, + .stretch => .stretch, + .none => .none, + }, + .repeat = self.config.bg_image_repeat, + }, + }; + // Signal that the buffer was modified. + self.bg_image_buffer_modified +%= 1; } /// Update uniforms for the custom shaders, if necessary. /// /// This should be called exactly once per frame, inside `drawFrame`. - fn updateCustomShaderUniforms( - self: *Self, - ) !void { + fn updateCustomShaderUniforms(self: *Self) !void { // We only need to do this if we have custom shaders. if (!self.has_custom_shaders) return; 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/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 //-------------------------------------------------------------------