mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 08:01:45 +00:00
terminal/kitty: switch to wuffs for pixel work
This commit is contained in:
@@ -55,6 +55,35 @@ pub fn bgraToRgba(alloc: Allocator, src: []const u8) Error![]u8 {
|
||||
);
|
||||
}
|
||||
|
||||
/// Composite `src` over `dst` in place. Both are straight
|
||||
/// (non-premultiplied) alpha RGBA of the same length. A transparent
|
||||
/// destination pixel takes the source pixel exactly; wuffs composites
|
||||
/// everything else in 16-bit integer space.
|
||||
pub fn rgbaSrcOver(dst: []u8, src: []const u8) void {
|
||||
assert(dst.len == src.len);
|
||||
assert(dst.len % 4 == 0);
|
||||
|
||||
var swizzler: c.wuffs_base__pixel_swizzler = undefined;
|
||||
const status = c.wuffs_base__pixel_swizzler__prepare(
|
||||
&swizzler,
|
||||
c.wuffs_base__make_pixel_format(c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL),
|
||||
c.wuffs_base__empty_slice_u8(),
|
||||
c.wuffs_base__make_pixel_format(c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL),
|
||||
c.wuffs_base__empty_slice_u8(),
|
||||
c.WUFFS_BASE__PIXEL_BLEND__SRC_OVER,
|
||||
);
|
||||
// This format pair and blend mode is a supported swizzle, so
|
||||
// preparation can only fail on a programming error.
|
||||
assert(c.wuffs_base__status__is_ok(&status));
|
||||
|
||||
_ = c.wuffs_base__pixel_swizzler__swizzle_interleaved_from_slice(
|
||||
&swizzler,
|
||||
c.wuffs_base__make_slice_u8(dst.ptr, dst.len),
|
||||
c.wuffs_base__empty_slice_u8(),
|
||||
c.wuffs_base__make_slice_u8(@constCast(src.ptr), src.len),
|
||||
);
|
||||
}
|
||||
|
||||
test "gaToRgba" {
|
||||
const rgba = try gaToRgba(std.testing.allocator, &.{ 7, 100, 8, 200 });
|
||||
defer std.testing.allocator.free(rgba);
|
||||
@@ -65,6 +94,30 @@ test "gaToRgba" {
|
||||
}, rgba);
|
||||
}
|
||||
|
||||
test "rgbaSrcOver" {
|
||||
// 50% white over opaque black, opaque over anything, transparent
|
||||
// source over anything, and anything over a transparent
|
||||
// destination (exact source passthrough).
|
||||
var dst = [_]u8{
|
||||
0, 0, 0, 255,
|
||||
10, 20, 30, 40,
|
||||
10, 20, 30, 40,
|
||||
0, 0, 0, 0,
|
||||
};
|
||||
rgbaSrcOver(&dst, &.{
|
||||
255, 255, 255, 128,
|
||||
100, 110, 120, 255,
|
||||
100, 110, 120, 0,
|
||||
200, 100, 50, 128,
|
||||
});
|
||||
try std.testing.expectEqualSlices(u8, &.{
|
||||
128, 128, 128, 255,
|
||||
100, 110, 120, 255,
|
||||
10, 20, 30, 40,
|
||||
200, 100, 50, 128,
|
||||
}, &dst);
|
||||
}
|
||||
|
||||
fn swizzle(
|
||||
alloc: Allocator,
|
||||
src: []const u8,
|
||||
|
||||
@@ -137,13 +137,18 @@ fn initVt(
|
||||
// We need uucode for grapheme break support
|
||||
vt.addImport("uucode", deps.uucode_mod);
|
||||
|
||||
// We need for Kitty graphics. If Kitty graphics is disabled then
|
||||
// z2d isn't referenced and it produces no code, so its safe.
|
||||
if (b.lazyDependency("z2d", .{
|
||||
.target = cfg.target,
|
||||
.optimize = cfg.optimize,
|
||||
})) |dep| {
|
||||
vt.addImport("z2d", dep.module("z2d"));
|
||||
// We need wuffs for Kitty graphics pixel operations (format
|
||||
// conversion and alpha blending). Unlike pure Zig dependencies
|
||||
// its C code is compiled whenever the module is in the build
|
||||
// graph regardless of analysis, so only wire it in when Kitty
|
||||
// graphics is actually enabled.
|
||||
if (vt_options.kittyGraphics(cfg.target.result)) {
|
||||
if (b.lazyDependency("wuffs", .{
|
||||
.target = cfg.target,
|
||||
.optimize = cfg.optimize,
|
||||
})) |dep| {
|
||||
vt.addImport("wuffs", dep.module("wuffs"));
|
||||
}
|
||||
}
|
||||
|
||||
// If SIMD is enabled, add all our SIMD dependencies.
|
||||
|
||||
@@ -257,6 +257,17 @@ pub const Options = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Whether the Kitty graphics feature is effectively enabled for
|
||||
/// the given target. Kitty graphics requires the ability to get
|
||||
/// timestamps and there is no way to do that on freestanding
|
||||
/// targets, so it is always disabled there regardless of the
|
||||
/// feature setting.
|
||||
pub fn kittyGraphics(self: Options, target: std.Target) bool {
|
||||
if (target.cpu.arch == .wasm32 and target.os.tag == .freestanding)
|
||||
return false;
|
||||
return self.features.kitty_graphics;
|
||||
}
|
||||
|
||||
/// Add the required build options for the terminal module.
|
||||
///
|
||||
/// The memory referenced by self is expected to stick around (it isn't
|
||||
@@ -281,12 +292,10 @@ pub const Options = struct {
|
||||
inline for (@typeInfo(Features).@"struct".fields) |field| {
|
||||
var value = @field(self.features, field.name);
|
||||
|
||||
// Kitty graphics requires the ability to get timestamps and
|
||||
// there is no way to do that on freestanding targets, so it
|
||||
// is always disabled there regardless of the feature setting.
|
||||
// Kitty graphics is force-disabled on some targets; see
|
||||
// kittyGraphics for details.
|
||||
if (comptime std.mem.eql(u8, field.name, "kitty_graphics")) {
|
||||
if (target.cpu.arch == .wasm32 and target.os.tag == .freestanding)
|
||||
value = false;
|
||||
value = self.kittyGraphics(target);
|
||||
}
|
||||
|
||||
opts.addOption(bool, field.name, value);
|
||||
|
||||
@@ -3,75 +3,39 @@
|
||||
//! These are the primitives behind animation frame loading (a=f) and
|
||||
//! frame composition (a=c). See graphics_animation.zig for the
|
||||
//! animation model built on top of them.
|
||||
//!
|
||||
//! All buffers are straight (non-premultiplied) alpha RGBA, as the
|
||||
//! protocol and our consumers (renderer, C API) expect.
|
||||
//! Since z2d composites in premultiplied alpha, blending round-trips
|
||||
//! each pixel through multiply/demultiply.
|
||||
//!
|
||||
//! Note, there's a lot here that is suboptimal (non-vectorized) and we
|
||||
//! should use something like wuffs probably too, but wuffs has a libc
|
||||
//! dependency we don't want to force. We can also optimize this later
|
||||
//! once we prove it all works.
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const z2d = @import("z2d");
|
||||
const wuffs = @import("wuffs");
|
||||
const command = @import("graphics_command.zig");
|
||||
|
||||
/// Convert pixel data in the given format to a freshly allocated RGBA
|
||||
/// buffer. The caller owns the result; the input is not freed.
|
||||
///
|
||||
/// These stay hand-rolled rather than using wuffs' swizzler because
|
||||
/// wuffs requires libc and libghostty-vt must remain buildable fully
|
||||
/// freestanding (see the module doc). They produce identical values.
|
||||
/// The input length must be a multiple of the format's bytes per
|
||||
/// pixel; image loading validates data length against the image
|
||||
/// dimensions before storing it.
|
||||
pub fn rgbaFromFormat(
|
||||
alloc: Allocator,
|
||||
format: command.Transmission.Format,
|
||||
data: []const u8,
|
||||
) Allocator.Error![]u8 {
|
||||
switch (format) {
|
||||
const result = switch (format) {
|
||||
.rgba => return try alloc.dupe(u8, data),
|
||||
|
||||
.rgb => {
|
||||
const pixels = data.len / 3;
|
||||
const result = try alloc.alloc(u8, pixels * 4);
|
||||
for (0..pixels) |i| {
|
||||
result[i * 4 + 0] = data[i * 3 + 0];
|
||||
result[i * 4 + 1] = data[i * 3 + 1];
|
||||
result[i * 4 + 2] = data[i * 3 + 2];
|
||||
result[i * 4 + 3] = 255;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
.gray => {
|
||||
const result = try alloc.alloc(u8, data.len * 4);
|
||||
for (data, 0..) |v, i| {
|
||||
result[i * 4 + 0] = v;
|
||||
result[i * 4 + 1] = v;
|
||||
result[i * 4 + 2] = v;
|
||||
result[i * 4 + 3] = 255;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
.gray_alpha => {
|
||||
const pixels = data.len / 2;
|
||||
const result = try alloc.alloc(u8, pixels * 4);
|
||||
for (0..pixels) |i| {
|
||||
const v = data[i * 2];
|
||||
result[i * 4 + 0] = v;
|
||||
result[i * 4 + 1] = v;
|
||||
result[i * 4 + 2] = v;
|
||||
result[i * 4 + 3] = data[i * 2 + 1];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
.rgb => wuffs.swizzle.rgbToRgba(alloc, data),
|
||||
.gray => wuffs.swizzle.gToRgba(alloc, data),
|
||||
.gray_alpha => wuffs.swizzle.gaToRgba(alloc, data),
|
||||
|
||||
// PNG is decoded to RGBA during image loading.
|
||||
.png => unreachable,
|
||||
}
|
||||
};
|
||||
|
||||
return result catch |err| switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
// These are fixed, supported swizzles; wuffs cannot fail to
|
||||
// prepare them and nothing else in the conversion errors.
|
||||
error.WuffsError, error.Overflow => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Fill an RGBA buffer with the given background color.
|
||||
@@ -154,36 +118,10 @@ pub fn composeCanvasRect(
|
||||
fn composeRow(dst: []u8, src: []const u8, mode: command.CompositionMode) void {
|
||||
switch (mode) {
|
||||
.overwrite => @memcpy(dst, src),
|
||||
.alpha_blend => {
|
||||
var i: usize = 0;
|
||||
while (i < dst.len) : (i += 4) {
|
||||
blendPixel(dst[i..][0..4], src[i..][0..4]);
|
||||
}
|
||||
},
|
||||
.alpha_blend => wuffs.swizzle.rgbaSrcOver(dst, src),
|
||||
}
|
||||
}
|
||||
|
||||
/// Source-over blend of one straight-alpha RGBA pixel onto another,
|
||||
/// via z2d's compositor. z2d composites in premultiplied alpha, so
|
||||
/// the pixels round-trip through multiply/demultiply; see the module
|
||||
/// doc for how that compares to Kitty.
|
||||
fn blendPixel(dst: *[4]u8, src: *const [4]u8) void {
|
||||
// A fully transparent source pixel leaves the destination
|
||||
// untouched, exactly like Kitty. This also keeps the no-op case
|
||||
// free of the premultiply round-trip's rounding.
|
||||
if (src[3] == 0) return;
|
||||
|
||||
const src_px: z2d.pixel.RGBA = .{ .r = src[0], .g = src[1], .b = src[2], .a = src[3] };
|
||||
const dst_px: z2d.pixel.RGBA = .{ .r = dst[0], .g = dst[1], .b = dst[2], .a = dst[3] };
|
||||
const out = z2d.compositor.runPixel(
|
||||
.integer,
|
||||
dst_px.multiply().asPixel(),
|
||||
src_px.multiply().asPixel(),
|
||||
.src_over,
|
||||
).rgba.demultiply();
|
||||
dst.* = .{ out.r, out.g, out.b, out.a };
|
||||
}
|
||||
|
||||
test "rgba conversion" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@@ -255,34 +193,33 @@ test "alpha blend source-over semantics" {
|
||||
// Opaque source overwrites exactly.
|
||||
{
|
||||
var dst = [4]u8{ 10, 20, 30, 40 };
|
||||
blendPixel(&dst, &.{ 100, 110, 120, 255 });
|
||||
composeRect(&dst, 1, 1, &.{ 100, 110, 120, 255 }, 1, 1, 0, 0, .alpha_blend);
|
||||
try testing.expectEqualSlices(u8, &.{ 100, 110, 120, 255 }, &dst);
|
||||
}
|
||||
|
||||
// Fully transparent source leaves the destination untouched
|
||||
// exactly (no premultiply round-trip; matches Kitty).
|
||||
// (like Kitty; see the module doc on rounding).
|
||||
{
|
||||
var dst = [4]u8{ 10, 20, 30, 40 };
|
||||
blendPixel(&dst, &.{ 100, 110, 120, 0 });
|
||||
composeRect(&dst, 1, 1, &.{ 100, 110, 120, 0 }, 1, 1, 0, 0, .alpha_blend);
|
||||
try testing.expectEqualSlices(u8, &.{ 10, 20, 30, 40 }, &dst);
|
||||
}
|
||||
|
||||
// 50% source over opaque destination.
|
||||
{
|
||||
var dst = [4]u8{ 0, 0, 0, 255 };
|
||||
blendPixel(&dst, &.{ 255, 255, 255, 128 });
|
||||
composeRect(&dst, 1, 1, &.{ 255, 255, 255, 128 }, 1, 1, 0, 0, .alpha_blend);
|
||||
try testing.expectEqual(@as(u8, 255), dst[3]);
|
||||
try testing.expectEqual(@as(u8, 128), dst[0]);
|
||||
}
|
||||
|
||||
// Blending over a transparent destination yields the source,
|
||||
// minus one bit of rounding on the color channels from z2d's
|
||||
// premultiply round-trip (Kitty's float math yields the source
|
||||
// exactly here; see the module doc).
|
||||
// Blending over a transparent destination yields the source
|
||||
// exactly, matching Kitty: wuffs passes the source through
|
||||
// untouched when the destination is transparent.
|
||||
{
|
||||
var dst = [4]u8{ 0, 0, 0, 0 };
|
||||
blendPixel(&dst, &.{ 200, 100, 50, 128 });
|
||||
try testing.expectEqualSlices(u8, &.{ 199, 99, 49, 128 }, &dst);
|
||||
composeRect(&dst, 1, 1, &.{ 200, 100, 50, 128 }, 1, 1, 0, 0, .alpha_blend);
|
||||
try testing.expectEqualSlices(u8, &.{ 200, 100, 50, 128 }, &dst);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user