foot/pgo/pgo.c
Daniel Eklöf ccf625b991
render: gamma-correct blending
This implements gamma-correct blending, which mainly affects font
rendering.

The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).

How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.

Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.

Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.

Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.

There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.

Perhaps, in the future, we can enable it by default if:

* gamma-correct blending is enabled
* the user has not enabled a transparent background
2025-03-05 18:45:01 +01:00

437 lines
10 KiB
C

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include "async.h"
#include "config.h"
#include "key-binding.h"
#include "reaper.h"
#include "sixel.h"
#include "user-notification.h"
#include "vt.h"
extern bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data);
static void
usage(const char *prog_name)
{
printf(
"Usage: %s stimuli-file1 stimuli-file2 ... stimuli-fileN\n",
prog_name);
}
enum async_write_status
async_write(int fd, const void *data, size_t len, size_t *idx)
{
return ASYNC_WRITE_DONE;
}
bool
fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data)
{
return true;
}
bool
fdm_del(struct fdm *fdm, int fd)
{
return true;
}
bool
fdm_event_add(struct fdm *fdm, int fd, int events)
{
return true;
}
bool
fdm_event_del(struct fdm *fdm, int fd, int events)
{
return true;
}
bool
render_resize(
struct terminal *term, int width, int height, uint8_t resize_options)
{
return true;
}
void render_refresh(struct terminal *term) {}
void render_refresh_csd(struct terminal *term) {}
void render_refresh_title(struct terminal *term) {}
void render_refresh_app_id(struct terminal *term) {}
void render_refresh_icon(struct terminal *term) {}
void render_overlay(struct terminal *term) {}
bool
render_xcursor_is_valid(const struct seat *seat, const char *cursor)
{
return true;
}
bool
render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape)
{
return true;
}
enum cursor_shape
xcursor_for_csd_border(struct terminal *term, int x, int y)
{
return CURSOR_SHAPE_LEFT_PTR;
}
struct wl_window *
wayl_win_init(struct terminal *term, const char *token)
{
return NULL;
}
void wayl_win_destroy(struct wl_window *win) {}
void wayl_win_alpha_changed(struct wl_window *win) {}
bool wayl_win_set_urgent(struct wl_window *win) { return true; }
bool wayl_win_ring_bell(const struct wl_window *win) { return true; }
bool wayl_fractional_scaling(const struct wayland *wayl) { return true; }
pid_t
spawn(struct reaper *reaper, const char *cwd, char *const argv[],
int stdin_fd, int stdout_fd, int stderr_fd,
reaper_cb cb, void *cb_data, const char *xdg_activation_token)
{
return 2;
}
pid_t
slave_spawn(
int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp,
const env_var_list_t *extra_env_vars, const char *term_env,
const char *conf_shell, bool login_shell,
const user_notifications_t *notifications)
{
return 0;
}
int
render_worker_thread(void *_ctx)
{
return 0;
}
bool
render_do_linear_blending(const struct terminal *term)
{
return false;
}
struct extraction_context *
extract_begin(enum selection_kind kind, bool strip_trailing_empty)
{
return NULL;
}
bool
extract_one(
const struct terminal *term, const struct row *row, const struct cell *cell,
int col, void *context)
{
return true;
}
bool
extract_finish(struct extraction_context *context, char **text, size_t *len)
{
return true;
}
void cmd_scrollback_up(struct terminal *term, int rows) {}
void cmd_scrollback_down(struct terminal *term, int rows) {}
void ime_enable(struct seat *seat) {}
void ime_disable(struct seat *seat) {}
void ime_reset_preedit(struct seat *seat) {}
bool
notify_notify(struct terminal *term, struct notification *notif)
{
return true;
}
void
notify_close(struct terminal *term, const char *id)
{
}
void
notify_free(struct terminal *term, struct notification *notif)
{
}
void
notify_icon_add(struct terminal *term, const char *id,
const char *symbolic_name, const uint8_t *data,
size_t data_sz)
{
}
void
notify_icon_del(struct terminal *term, const char *id)
{
}
void
notify_icon_free(struct notification_icon *icon)
{
}
void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {}
void reaper_del(struct reaper *reaper, pid_t pid) {}
void urls_reset(struct terminal *term) {}
void shm_unref(struct buffer *buf) {}
void shm_chain_free(struct buffer_chain *chain) {}
struct buffer_chain *
shm_chain_new(
struct wayland *wayl, bool scrollable, size_t pix_instances,
bool ten_bit_it_if_capable)
{
return NULL;
}
void search_selection_cancelled(struct terminal *term) {}
void get_current_modifiers(const struct seat *seat,
xkb_mod_mask_t *effective,
xkb_mod_mask_t *consumed, uint32_t key,
bool filter_locked) {}
static struct key_binding_set kbd;
static bool kbd_initialized = false;
struct key_binding_set *
key_binding_for(
struct key_binding_manager *mgr, const struct config *conf,
const struct seat *seat)
{
return &kbd;
}
void
key_binding_new_for_conf(
struct key_binding_manager *mgr, const struct wayland *wayl,
const struct config *conf)
{
if (!kbd_initialized) {
kbd_initialized = true;
kbd = (struct key_binding_set){
.key = tll_init(),
.search = tll_init(),
.url = tll_init(),
.mouse = tll_init(),
.selection_overrides = 0,
};
}
}
void
key_binding_unref(struct key_binding_manager *mgr, const struct config *conf)
{
}
int
main(int argc, const char *const *argv)
{
if (argc < 2) {
usage(argv[0]);
return EXIT_FAILURE;
}
const int row_count = 67;
const int col_count = 135;
const int grid_row_count = 16384;
int lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
if (lower_fd < 0)
return EXIT_FAILURE;
int upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
if (upper_fd < 0) {
close(lower_fd);
return EXIT_FAILURE;
}
struct row **normal_rows = calloc(grid_row_count, sizeof(normal_rows[0]));
struct row **alt_rows = calloc(grid_row_count, sizeof(alt_rows[0]));
for (int i = 0; i < grid_row_count; i++) {
normal_rows[i] = calloc(1, sizeof(*normal_rows[i]));
normal_rows[i]->cells = calloc(col_count, sizeof(normal_rows[i]->cells[0]));
alt_rows[i] = calloc(1, sizeof(*alt_rows[i]));
alt_rows[i]->cells = calloc(col_count, sizeof(alt_rows[i]->cells[0]));
}
struct config conf = {
.tweak = {
.delayed_render_lower_ns = 500000, /* 0.5ms */
.delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */
},
};
struct wayland wayl = {
.seats = tll_init(),
.monitors = tll_init(),
.terms = tll_init(),
};
struct terminal term = {
.conf = &conf,
.wl = &wayl,
.grid = &term.normal,
.normal = {
.num_rows = grid_row_count,
.num_cols = col_count,
.rows = normal_rows,
.cur_row = normal_rows[0],
},
.alt = {
.num_rows = grid_row_count,
.num_cols = col_count,
.rows = alt_rows,
.cur_row = alt_rows[0],
},
.scale = 1,
.width = col_count * 8,
.height = row_count * 15,
.cols = col_count,
.rows = row_count,
.cell_width = 8,
.cell_height = 15,
.scroll_region = {
.start = 0,
.end = row_count,
},
.selection = {
.coords = {
.start = {-1, -1},
.end = {-1, -1},
},
},
.delayed_render_timer = {
.lower_fd = lower_fd,
.upper_fd = upper_fd
},
.sixel = {
.palette_size = SIXEL_MAX_COLORS,
.max_width = SIXEL_MAX_WIDTH,
.max_height = SIXEL_MAX_HEIGHT,
},
};
tll_push_back(wayl.terms, &term);
int ret = EXIT_FAILURE;
for (int i = 1; i < argc; i++) {
struct stat st;
if (stat(argv[i], &st) < 0) {
fprintf(stderr, "error: %s: failed to stat: %s\n",
argv[i], strerror(errno));
goto out;
}
uint8_t *data = malloc(st.st_size);
if (data == NULL) {
fprintf(stderr, "error: %s: failed to allocate buffer: %s\n",
argv[i], strerror(errno));
goto out;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
fprintf(stderr, "error: %s: failed to open: %s\n",
argv[i], strerror(errno));
goto out;
}
ssize_t amount = read(fd, data, st.st_size);
if (amount != st.st_size) {
fprintf(stderr, "error: %s: failed to read: %s\n",
argv[i], strerror(errno));
goto out;
}
close(fd);
#if defined(MEMFD_CREATE)
int mem_fd = memfd_create("foot-pgo-ptmx", MFD_CLOEXEC);
#elif defined(__FreeBSD__)
// memfd_create on FreeBSD 13 is SHM_ANON without sealing support
int mem_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600);
#else
char name[] = "/tmp/foot-pgo-ptmx-XXXXXX";
int mem_fd = mkostemp(name, O_CLOEXEC);
unlink(name);
#endif
if (mem_fd < 0) {
fprintf(stderr, "error: failed to create memory FD\n");
goto out;
}
if (write(mem_fd, data, st.st_size) < 0) {
fprintf(stderr, "error: failed to write memory FD\n");
close(mem_fd);
goto out;
}
free(data);
term.ptmx = mem_fd;
lseek(mem_fd, 0, SEEK_SET);
printf("Feeding VT parser with %s (%lld bytes)\n",
argv[i], (long long)st.st_size);
while (lseek(mem_fd, 0, SEEK_CUR) < st.st_size) {
if (!fdm_ptmx(NULL, -1, EPOLLIN, &term)) {
fprintf(stderr, "error: fdm_ptmx() failed\n");
close(mem_fd);
goto out;
}
}
close(mem_fd);
}
ret = EXIT_SUCCESS;
out:
tll_free(wayl.terms);
for (int i = 0; i < grid_row_count; i++) {
if (normal_rows[i] != NULL)
free(normal_rows[i]->cells);
free(normal_rows[i]);
if (alt_rows[i] != NULL)
free(alt_rows[i]->cells);
free(alt_rows[i]);
}
free(normal_rows);
free(alt_rows);
close(lower_fd);
close(upper_fd);
return ret;
}