diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | external/wlr-layer-shell-unstable-v1.xml | 311 | ||||
-rwxr-xr-x | generate-version.sh | 38 | ||||
-rw-r--r-- | log.c | 166 | ||||
-rw-r--r-- | log.h | 42 | ||||
-rw-r--r-- | main.c | 421 | ||||
-rw-r--r-- | meson.build | 98 | ||||
-rw-r--r-- | png-wbg.h | 5 | ||||
-rw-r--r-- | png.c | 143 | ||||
-rw-r--r-- | shm.c | 192 | ||||
-rw-r--r-- | shm.h | 25 | ||||
-rw-r--r-- | stride.h | 9 |
13 files changed, 1472 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f2ded7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/bld
\ No newline at end of file @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Daniel Eklöf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/wlr-layer-shell-unstable-v1.xml b/external/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..fa67001 --- /dev/null +++ b/external/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,311 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="wlr_layer_shell_unstable_v1"> + <copyright> + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + </copyright> + + <interface name="zwlr_layer_shell_v1" version="3"> + <description summary="create surfaces that are layers of the desktop"> + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + </description> + + <request name="get_layer_surface"> + <description summary="create a layer_surface from a surface"> + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + </description> + <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/> + <arg name="surface" type="object" interface="wl_surface"/> + <arg name="output" type="object" interface="wl_output" allow-null="true"/> + <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/> + <arg name="namespace" type="string" summary="namespace for the layer surface"/> + </request> + + <enum name="error"> + <entry name="role" value="0" summary="wl_surface has another role"/> + <entry name="invalid_layer" value="1" summary="layer value is invalid"/> + <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/> + </enum> + + <enum name="layer"> + <description summary="available layers for surfaces"> + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + </description> + + <entry name="background" value="0"/> + <entry name="bottom" value="1"/> + <entry name="top" value="2"/> + <entry name="overlay" value="3"/> + </enum> + + <!-- Version 3 additions --> + + <request name="destroy" type="destructor" since="3"> + <description summary="destroy the layer_shell object"> + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + </description> + </request> + </interface> + + <interface name="zwlr_layer_surface_v1" version="3"> + <description summary="layer metadata interface"> + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + </description> + + <request name="set_size"> + <description summary="sets the size of the surface"> + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + </description> + <arg name="width" type="uint"/> + <arg name="height" type="uint"/> + </request> + + <request name="set_anchor"> + <description summary="configures the anchor point of the surface"> + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + </description> + <arg name="anchor" type="uint" enum="anchor"/> + </request> + + <request name="set_exclusive_zone"> + <description summary="configures the exclusive geometry of this surface"> + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + </description> + <arg name="zone" type="int"/> + </request> + + <request name="set_margin"> + <description summary="sets a margin from the anchor point"> + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + </description> + <arg name="top" type="int"/> + <arg name="right" type="int"/> + <arg name="bottom" type="int"/> + <arg name="left" type="int"/> + </request> + + <request name="set_keyboard_interactivity"> + <description summary="requests keyboard events"> + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + </description> + <arg name="keyboard_interactivity" type="uint"/> + </request> + + <request name="get_popup"> + <description summary="assign this layer_surface as an xdg_popup parent"> + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + </description> + <arg name="popup" type="object" interface="xdg_popup"/> + </request> + + <request name="ack_configure"> + <description summary="ack a configure event"> + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + </description> + <arg name="serial" type="uint" summary="the serial from the configure event"/> + </request> + + <request name="destroy" type="destructor"> + <description summary="destroy the layer_surface"> + This request destroys the layer surface. + </description> + </request> + + <event name="configure"> + <description summary="suggest a surface change"> + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + </description> + <arg name="serial" type="uint"/> + <arg name="width" type="uint"/> + <arg name="height" type="uint"/> + </event> + + <event name="closed"> + <description summary="surface should be closed"> + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + </description> + </event> + + <enum name="error"> + <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/> + <entry name="invalid_size" value="1" summary="size is invalid"/> + <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/> + </enum> + + <enum name="anchor" bitfield="true"> + <entry name="top" value="1" summary="the top edge of the anchor rectangle"/> + <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/> + <entry name="left" value="4" summary="the left edge of the anchor rectangle"/> + <entry name="right" value="8" summary="the right edge of the anchor rectangle"/> + </enum> + + <!-- Version 2 additions --> + + <request name="set_layer" since="2"> + <description summary="change the layer of the surface"> + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + </description> + <arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/> + </request> + </interface> +</protocol> diff --git a/generate-version.sh b/generate-version.sh new file mode 100755 index 0000000..c77560a --- /dev/null +++ b/generate-version.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +set -e + +default_version=${1} +src_dir=${2} +out_file=${3} + +# echo "default version: ${default_version}" +# echo "source directory: ${src_dir}" +# echo "output file: ${out_file}" + +if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then + workdir=$(pwd) + cd "${src_dir}" + git_version=$(git describe --always --tags) + git_branch=$(git rev-parse --abbrev-ref HEAD) + cd "${workdir}" + + new_version="${git_version} ($(env LC_TIME=C date "+%b %d %Y"), branch '${git_branch}')" +else + new_version="${default_version}" +fi + +new_version="#define FUZZEL_VERSION \"${new_version}\"" + +if [ -f "${out_file}" ]; then + old_version=$(cat "${out_file}") +else + old_version="" +fi + +# echo "old version: ${old_version}" +# echo "new version: ${new_version}" + +if [ "${old_version}" != "${new_version}" ]; then + echo "${new_version}" > "${out_file}" +fi @@ -0,0 +1,166 @@ +#include "log.h" + +#include <stdlib.h> +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <stdbool.h> +#include <errno.h> +#include <assert.h> +#include <unistd.h> + +#include <syslog.h> + +static bool colorize = false; +static bool do_syslog = true; + +void +log_init(enum log_colorize _colorize, bool _do_syslog, + enum log_facility syslog_facility, enum log_class syslog_level) +{ + static const int facility_map[] = { + [LOG_FACILITY_USER] = LOG_USER, + [LOG_FACILITY_DAEMON] = LOG_DAEMON, + }; + + static const int level_map[] = { + [LOG_CLASS_ERROR] = LOG_ERR, + [LOG_CLASS_WARNING] = LOG_WARNING, + [LOG_CLASS_INFO] = LOG_INFO, + [LOG_CLASS_DEBUG] = LOG_DEBUG, + }; + + colorize = _colorize == LOG_COLORIZE_NEVER ? false : _colorize == LOG_COLORIZE_ALWAYS ? true : isatty(STDERR_FILENO); + do_syslog = _do_syslog; + + if (do_syslog) { + openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); + setlogmask(LOG_UPTO(level_map[syslog_level])); + } +} + +void +log_deinit(void) +{ + if (do_syslog) + closelog(); +} + +static void +_log(enum log_class log_class, const char *module, const char *file, int lineno, + const char *fmt, int sys_errno, va_list va) +{ + const char *class = "abcd"; + int class_clr = 0; + switch (log_class) { + case LOG_CLASS_ERROR: class = " err"; class_clr = 31; break; + case LOG_CLASS_WARNING: class = "warn"; class_clr = 33; break; + case LOG_CLASS_INFO: class = "info"; class_clr = 97; break; + case LOG_CLASS_DEBUG: class = " dbg"; class_clr = 36; break; + } + + char clr[16]; + snprintf(clr, sizeof(clr), "\e[%dm", class_clr); + fprintf(stderr, "%s%s%s: ", colorize ? clr : "", class, colorize ? "\e[0m" : ""); + + if (colorize) + fprintf(stderr, "\e[2m"); + fprintf(stderr, "%s:%d: ", file, lineno); + if (colorize) + fprintf(stderr, "\e[0m"); + + vfprintf(stderr, fmt, va); + + if (sys_errno != 0) + fprintf(stderr, ": %s", strerror(sys_errno)); + + fprintf(stderr, "\n"); +} + +static void +_sys_log(enum log_class log_class, const char *module, + const char *file __attribute__((unused)), + int lineno __attribute__((unused)), + const char *fmt, int sys_errno, va_list va) +{ + if (!do_syslog) + return; + + /* Map our log level to syslog's level */ + int level = -1; + switch (log_class) { + case LOG_CLASS_ERROR: level = LOG_ERR; break; + case LOG_CLASS_WARNING: level = LOG_WARNING; break; + case LOG_CLASS_INFO: level = LOG_INFO; break; + case LOG_CLASS_DEBUG: level = LOG_DEBUG; break; + } + + assert(level != -1); + + const char *sys_err = sys_errno != 0 ? strerror(sys_errno) : NULL; + + va_list va2; + va_copy(va2, va); + + /* Calculate required size of buffer holding the entire log message */ + int required_len = 0; + required_len += strlen(module) + 2; /* "%s: " */ + required_len += vsnprintf(NULL, 0, fmt, va2); va_end(va2); + + if (sys_errno != 0) + required_len += strlen(sys_err) + 2; /* ": %s" */ + + /* Format the msg */ + char *msg = malloc(required_len + 1); + int idx = 0; + + idx += snprintf(&msg[idx], required_len + 1 - idx, "%s: ", module); + idx += vsnprintf(&msg[idx], required_len + 1 - idx, fmt, va); + + if (sys_errno != 0) { + snprintf( + &msg[idx], required_len + 1 - idx, ": %s", strerror(sys_errno)); + } + + syslog(level, "%s", msg); + free(msg); +} + +void +log_msg(enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, ...) +{ + va_list ap1, ap2; + va_start(ap1, fmt); + va_copy(ap2, ap1); + _log(log_class, module, file, lineno, fmt, 0, ap1); + _sys_log(log_class, module, file, lineno, fmt, 0, ap2); + va_end(ap1); + va_end(ap2); +} + +void log_errno(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) +{ + va_list ap1, ap2; + va_start(ap1, fmt); + va_copy(ap2, ap1); + _log(log_class, module, file, lineno, fmt, errno, ap1); + _sys_log(log_class, module, file, lineno, fmt, errno, ap2); + va_end(ap1); + va_end(ap2); +} + +void log_errno_provided(enum log_class log_class, const char *module, + const char *file, int lineno, int _errno, + const char *fmt, ...) +{ + va_list ap1, ap2; + va_start(ap1, fmt); + va_copy(ap2, ap1); + _log(log_class, module, file, lineno, fmt, _errno, ap1); + _sys_log(log_class, module, file, lineno, fmt, _errno, ap2); + va_end(ap1); + va_end(ap2); +} @@ -0,0 +1,42 @@ +#pragma once +#include <stdbool.h> + +enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; +enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; +enum log_class { LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG }; + +void log_init(enum log_colorize colorize, bool do_syslog, + enum log_facility syslog_facility, enum log_class syslog_level); +void log_deinit(void); + +void log_msg(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) __attribute__((format (printf, 5, 6))); + +void log_errno(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) __attribute__((format (printf, 5, 6))); + +void log_errno_provided( + enum log_class log_class, const char *module, + const char *file, int lineno, int _errno, + const char *fmt, ...) __attribute__((format (printf, 6, 7))); + +#define LOG_ERR(fmt, ...) \ + log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) +#define LOG_ERRNO(fmt, ...) \ + log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) +#define LOG_ERRNO_P(fmt, _errno, ...) \ + log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ + _errno, fmt, ## __VA_ARGS__) +#define LOG_WARN(fmt, ...) \ + log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) +#define LOG_INFO(fmt, ...) \ + log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + #define LOG_DBG(fmt, ...) \ + log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, fmt, ## __VA_ARGS__) +#else + #define LOG_DBG(fmt, ...) +#endif @@ -0,0 +1,421 @@ +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <poll.h> +#include <errno.h> +#include <signal.h> +#include <unistd.h> +#include <locale.h> +#include <assert.h> + +#include <sys/signalfd.h> + +#include <wayland-client.h> +#include <wayland-cursor.h> + +#include <wlr-layer-shell-unstable-v1.h> +#include <pixman.h> +#include <tllist.h> + +#define LOG_MODULE "wbg" +#define LOG_ENABLE_DBG 1 +#include "log.h" +#include "png-wbg.h" +#include "shm.h" + +/* Top-level globals */ +static struct wl_display *display; +static struct wl_registry *registry; +static struct wl_compositor *compositor; +static struct wl_shm *shm; +static struct zwlr_layer_shell_v1 *layer_shell; + +static bool have_argb8888 = false; + +/* TODO: one per output */ +static pixman_image_t *image; + +struct output { + struct wl_output *wl_output; + uint32_t wl_name; + + char *make; + char *model; + + int scale; + int width; + int height; + + int render_width; + int render_height; + + struct wl_surface *surf; + struct zwlr_layer_surface_v1 *layer; +}; +static tll(struct output) outputs; + +static void +render(struct output *output) +{ + const int width = output->render_width; + const int height = output->render_height; + const int scale = output->scale; + + struct buffer *buf = shm_get_buffer( + shm, width * scale, height * scale, (uintptr_t)(void *)output); + assert(buf != NULL); + + LOG_INFO("render: w=%d, h=%d", width * scale, height * scale); + + pixman_color_t color = {.red = 0xffff, .green = 0, .blue = 0, .alpha = 0xffff}; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &color, 1, + &(pixman_rectangle16_t){0, 0, width * scale, height * scale}); + + uint32_t *data = pixman_image_get_data(image); + int img_width = pixman_image_get_width(image); + int img_height = pixman_image_get_height(image); + int img_stride = pixman_image_get_stride(image); + pixman_format_code_t img_fmt = pixman_image_get_format(image); + + double sx = (double)img_width / width; + double sy = (double)img_height / height; + + pixman_image_t *pix = pixman_image_create_bits_no_clear( + img_fmt, img_width, img_height, data, img_stride); + pixman_f_transform_t t; + pixman_transform_t t2; + pixman_f_transform_init_scale(&t, sx, sy); + pixman_transform_from_pixman_f_transform(&t2, &t); + pixman_image_set_transform(pix, &t2); + + pixman_image_composite32( + PIXMAN_OP_SRC, + pix, NULL, buf->pix, 0, 0, 0, 0, 0, 0, + width, height); + + pixman_image_unref(pix); + + wl_surface_set_buffer_scale(output->surf, scale); + wl_surface_attach(output->surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(output->surf, 0, 0, width * scale, height * scale); + wl_surface_commit(output->surf); +} + +static void +layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, + uint32_t serial, uint32_t w, uint32_t h) +{ + struct output *output = data; + output->render_width = w; + output->render_height = h; + zwlr_layer_surface_v1_ack_configure(surface, serial); + render(output); +} + +static void +layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) +{ +} + +static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = &layer_surface_configure, + .closed = &layer_surface_closed, +}; + +static void +output_destroy(struct output *output) +{ + free(output->make); + free(output->model); + if (output->layer != NULL) + zwlr_layer_surface_v1_destroy(output->layer); + if (output->surf != NULL) + wl_surface_destroy(output->surf); + if (output->wl_output != NULL) + wl_output_destroy(output->wl_output); +} + +static void +output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, + int32_t physical_width, int32_t physical_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) +{ + struct output *output = data; + + free(output->make); + free(output->model); + + output->make = make != NULL ? strdup(make) : NULL; + output->model = model != NULL ? strdup(model) : NULL; +} + +static void +output_mode(void *data, struct wl_output *wl_output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) + return; + + struct output *output = data; + output->width = width; + output->height = height; +} + +static void +output_done(void *data, struct wl_output *wl_output) +{ + struct output *output = data; + const int width = output->width; + const int height = output->height; + const int scale = output->scale; + + LOG_INFO("output: %s %s (%dx%d, scale=%d)", + output->make, output->model, width, height, scale); + + assert(output->surf != NULL); + assert(output->layer != NULL); +} + +static void +output_scale(void *data, struct wl_output *wl_output, int32_t factor) +{ + struct output *output = data; + output->scale = factor; +} + +static const struct wl_output_listener output_listener = { + .geometry = &output_geometry, + .mode = &output_mode, + .done = &output_done, + .scale = &output_scale, +}; + +static void +shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) +{ + if (format == WL_SHM_FORMAT_ARGB8888) + have_argb8888 = true; +} + +static const struct wl_shm_listener shm_listener = { + .format = &shm_format, +}; + +static bool +verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) +{ + if (version >= wanted) + return true; + + LOG_ERR("%s: need interface version %u, but compositor only implements %u", + iface, wanted, version); + return false; +} + +static void +handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) +{ + if (strcmp(interface, wl_compositor_interface.name) == 0) { + const uint32_t required = 4; + if (!verify_iface_version(interface, version, required)) + return; + + compositor = wl_registry_bind( + registry, name, &wl_compositor_interface, required); + } + + else if (strcmp(interface, wl_shm_interface.name) == 0) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + shm = wl_registry_bind( + registry, name, &wl_shm_interface, required); + wl_shm_add_listener(shm, &shm_listener, NULL); + } + + else if (strcmp(interface, wl_output_interface.name) == 0) { + const uint32_t required = 3; + if (!verify_iface_version(interface, version, required)) + return; + + struct wl_output *wl_output = wl_registry_bind( + registry, name, &wl_output_interface, required); + + struct wl_surface *surf = wl_compositor_create_surface(compositor); + + /* Default input region is 'infinite', while we want it to be empty */ + struct wl_region *empty_region =wl_compositor_create_region(compositor); + wl_surface_set_input_region(surf, empty_region); + wl_region_destroy(empty_region); + + struct zwlr_layer_surface_v1 *layer = zwlr_layer_shell_v1_get_layer_surface( + layer_shell, surf, wl_output, ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "wallpaper"); + + zwlr_layer_surface_v1_set_exclusive_zone(layer, -1); + zwlr_layer_surface_v1_set_anchor(layer, + ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); + + tll_push_back(outputs, ((struct output){ + .wl_output = wl_output, .wl_name = name, .surf = surf, .layer = layer})); + struct output *output = &tll_back(outputs); + + wl_output_add_listener(wl_output, &output_listener, output); + zwlr_layer_surface_v1_add_listener(layer, &layer_surface_listener, output); + wl_surface_commit(surf); + } + + else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + const uint32_t required = 2; + if (!verify_iface_version(interface, version, required)) + return; + + layer_shell = wl_registry_bind( + registry, name, &zwlr_layer_shell_v1_interface, required); + } + +} + +static void +handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + tll_foreach(outputs, it) { + if (it->item.wl_name == name) { + LOG_DBG("destroyed: %s %s", it->item.make, it->item.model); + output_destroy(&it->item); + tll_remove(outputs, it); + return; + } + } +} + +static const struct wl_registry_listener registry_listener = { + .global = &handle_global, + .global_remove = &handle_global_remove, +}; + +int +main(int argc, const char *const *argv) +{ + if (argc < 2) { + LOG_ERR("missing required argument: image path"); + return EXIT_FAILURE; + } + + const char *image_path = argv[1]; + image = png_load(image_path); + if (image == NULL) { + LOG_ERR("%s: failed to load", image_path); + return EXIT_FAILURE; + } + + setlocale(LC_CTYPE, ""); + log_init(LOG_COLORIZE_AUTO, false, LOG_FACILITY_DAEMON, LOG_CLASS_WARNING); + + display = wl_display_connect(NULL); + assert(display != NULL); + + registry = wl_display_get_registry(display); + assert(registry != NULL); + + wl_registry_add_listener(registry, ®istry_listener, NULL); + wl_display_roundtrip(display); + + assert(compositor != NULL); + assert(shm != NULL); + assert(layer_shell != NULL); + + wl_display_roundtrip(display); + assert(have_argb8888); + + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGQUIT); + + sigprocmask(SIG_BLOCK, &mask, NULL); + + int sig_fd = signalfd(-1, &mask, 0); + assert(sig_fd >= 0); + + int exit_code = EXIT_FAILURE; + + while (true) { + wl_display_flush(display); + + struct pollfd fds[] = { + {.fd = wl_display_get_fd(display), .events = POLLIN}, + {.fd = sig_fd, .events = POLLIN}, + }; + int ret = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + + if (ret < 0) { + if (ret == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + break; + } + + if (fds[0].revents & POLLHUP) { + LOG_WARN("disconnected by compositor"); + break; + } + + if (fds[0].revents & POLLIN) + wl_display_dispatch(display); + + if (fds[1].revents & POLLHUP) + abort(); + + if (fds[1].revents & POLLIN) { + struct signalfd_siginfo info; + ssize_t count = read(sig_fd, &info, sizeof(info)); + if (count < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to read from signal FD"); + break; + } + + assert(count == sizeof(info)); + assert(info.ssi_signo == SIGINT || info.ssi_signo == SIGQUIT); + + LOG_INFO("goodbye"); + exit_code = EXIT_SUCCESS; + break; + } + } + + if (sig_fd >= 0) + close(sig_fd); + + tll_foreach(outputs, it) + output_destroy(&it->item); + tll_free(outputs); + + shm_fini(); + if (layer_shell != NULL) + zwlr_layer_shell_v1_destroy(layer_shell); + if (shm != NULL) + wl_shm_destroy(shm); + if (compositor != NULL) + wl_compositor_destroy(compositor); + if (registry != NULL) + wl_registry_destroy(registry); + if (display != NULL) + wl_display_disconnect(display); + if (image != NULL) { + free(pixman_image_get_data(image)); + pixman_image_unref(image); + } + log_deinit(); + return exit_code; +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..b11ffff --- /dev/null +++ b/meson.build @@ -0,0 +1,98 @@ +project('wbg', 'c', + version: '0.1.0', + license: 'MIT', + meson_version: '>=0.53.0', + default_options: [ + 'c_std=c18', + 'warning_level=1', + 'werror=true', + 'b_ndebug=if-release']) + +is_debug_build = get_option('buildtype').startswith('debug') + +add_project_arguments( + ['-D_POSIX_C_SOURCE=200809L', '-D_GNU_SOURCE'] + + (is_debug_build ? ['-D_DEBUG'] : []), + language: 'c', +) + +cc = meson.get_compiler('c') + +# Compute the relative path used by compiler invocations. +source_root = meson.current_source_dir().split('/') +build_root = meson.build_root().split('/') +relative_dir_parts = [] +i = 0 +in_prefix = true +foreach p : build_root + if i >= source_root.length() or not in_prefix or p != source_root[i] + in_prefix = false + relative_dir_parts += '..' + endif + i += 1 +endforeach +i = 0 +in_prefix = true +foreach p : source_root + if i >= build_root.length() or not in_prefix or build_root[i] != p + in_prefix = false + relative_dir_parts += p + endif + i += 1 +endforeach +relative_dir = join_paths(relative_dir_parts) + '/' + +if cc.has_argument('-fmacro-prefix-map=/foo=') + add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c') +endif + +pixman = dependency('pixman-1') +png = dependency('libpng') + +wayland_protocols = dependency('wayland-protocols') +wayland_client = dependency('wayland-client') +tllist = dependency('tllist', version: '>=1.0.1', fallback: 'tllist') + +wayland_protocols_datadir = wayland_protocols.get_pkgconfig_variable('pkgdatadir') + +wscanner = dependency('wayland-scanner', native: true) +wscanner_prog = find_program( + wscanner.get_pkgconfig_variable('wayland_scanner'), native: true) + +wl_proto_headers = [] +wl_proto_src = [] +foreach prot : [ + 'external/wlr-layer-shell-unstable-v1.xml', + wayland_protocols_datadir + '/stable/xdg-shell/xdg-shell.xml'] + + + wl_proto_headers += custom_target( + prot.underscorify() + '-client-header', + output: '@BASENAME@.h', + input: prot, + command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) + + wl_proto_src += custom_target( + prot.underscorify() + '-private-code', + output: '@BASENAME@.c', + input: prot, + command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) +endforeach + +generate_version_sh = files('generate-version.sh') +version = custom_target( + 'generate_version', + build_always_stale: true, + output: 'version.h', + command: [generate_version_sh, meson.project_version(), '@SOURCE_DIR@', '@OUTPUT@']) + +executable( + 'wbg', + 'main.c', + 'log.c', 'log.h', + 'png.c', 'png-wbg.h', + 'shm.c', 'shm.h', + 'stride.h', + wl_proto_src + wl_proto_headers, version, + dependencies: [pixman, png, wayland_client, tllist], + install: true) diff --git a/png-wbg.h b/png-wbg.h new file mode 100644 index 0000000..bf15f9d --- /dev/null +++ b/png-wbg.h @@ -0,0 +1,5 @@ +#pragma once + +#include <pixman.h> + +pixman_image_t *png_load(const char *path); @@ -0,0 +1,143 @@ +#include "png-wbg.h" + +#include <stdlib.h> +#include <stdio.h> +#include <stdbool.h> +#include <string.h> +#include <assert.h> + +#include <png.h> +#include <pixman.h> + +#define LOG_MODULE "png" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "stride.h" + +pixman_image_t * +png_load(const char *path) +{ + pixman_image_t *pix = NULL; + + FILE *fp = NULL; + png_structp png_ptr = NULL; + png_infop info_ptr = NULL; + png_bytepp row_pointers = NULL; + uint8_t *image_data = NULL; + + /* open file and test for it being a png */ + if ((fp = fopen(path, "rb")) == NULL) { + //LOG_ERRNO("%s: failed to open", path); + goto err; + } + + /* Verify PNG header */ + uint8_t header[8] = {0}; + if (fread(header, 1, 8, fp) != 8 || png_sig_cmp(header, 0, 8)) { + // LOG_ERR("%s: not a PNG", path); + goto err; + } + + /* Prepare for reading the PNG */ + if ((png_ptr = png_create_read_struct( + PNG_LIBPNG_VER_STRING, NULL, NULL, NULL)) == NULL || + (info_ptr = png_create_info_struct(png_ptr)) == NULL) + { + LOG_ERR("%s: failed to initialize libpng", path); + goto err; + } + + if (setjmp(png_jmpbuf(png_ptr))) { + LOG_ERR("%s: libpng error", path); + goto err; + } + + png_init_io(png_ptr, fp); + png_set_sig_bytes(png_ptr, 8); + + /* Get meta data */ + png_read_info(png_ptr, info_ptr); + int width = png_get_image_width(png_ptr, info_ptr); + int height = png_get_image_height(png_ptr, info_ptr); + png_byte color_type = png_get_color_type(png_ptr, info_ptr); + png_byte bit_depth __attribute__((unused)) = png_get_bit_depth(png_ptr, info_ptr); + int channels __attribute__((unused)) = png_get_channels(png_ptr, info_ptr); + + LOG_DBG("%s: %dx%d@%hhubpp, %d channels", path, width, height, bit_depth, channels); + + png_set_packing(png_ptr); + png_set_interlace_handling(png_ptr); + png_set_strip_16(png_ptr); /* "pack" 16-bit colors to 8-bit */ + png_set_bgr(png_ptr); + + /* pixman expects pre-multiplied alpha */ + png_set_alpha_mode(png_ptr, PNG_ALPHA_PREMULTIPLIED, 1.0); + + /* Tell libpng to expand to RGB(A) when necessary, and tell pixman + * whether we have alpha or not */ + pixman_format_code_t format; + switch (color_type) { + case PNG_COLOR_TYPE_GRAY: + case PNG_COLOR_TYPE_GRAY_ALPHA: + LOG_DBG("%d-bit gray%s", bit_depth, + color_type == PNG_COLOR_TYPE_GRAY_ALPHA ? "+alpha" : ""); + + if (bit_depth < 8) + png_set_expand_gray_1_2_4_to_8(png_ptr); + + png_set_gray_to_rgb(png_ptr); + format = color_type == PNG_COLOR_TYPE_GRAY ? PIXMAN_r8g8b8 : PIXMAN_a8r8g8b8; + break; + + case PNG_COLOR_TYPE_PALETTE: + LOG_DBG("%d-bit colormap%s", bit_depth, + png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS) ? "+tRNS" : ""); + + png_set_palette_to_rgb(png_ptr); + if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(png_ptr); + format = PIXMAN_a8r8g8b8; + } else + format = PIXMAN_r8g8b8; + break; + + case PNG_COLOR_TYPE_RGB: + LOG_DBG("RGB"); + format = PIXMAN_r8g8b8; + break; + + case PNG_COLOR_TYPE_RGBA: + LOG_DBG("RGBA"); + format = PIXMAN_a8r8g8b8; + break; + } + + png_read_update_info(png_ptr, info_ptr); + + size_t row_bytes __attribute__((unused)) = png_get_rowbytes(png_ptr, info_ptr); + int stride = stride_for_format_and_width(format, width); + image_data = malloc(height * stride); + + LOG_DBG("stride=%d, row-bytes=%zu", stride, row_bytes); + assert(stride >= row_bytes); + + row_pointers = malloc(height * sizeof(png_bytep)); + for (int i = 0; i < height; i++) + row_pointers[i] = &image_data[i * stride]; + + png_read_image(png_ptr, row_pointers); + + pix = pixman_image_create_bits_no_clear( + format, width, height, (uint32_t *)image_data, stride); + +err: + if (pix == NULL) + free(image_data); + free(row_pointers); + if (png_ptr != NULL) + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + if (fp != NULL) + fclose(fp); + + return pix; +} @@ -0,0 +1,192 @@ +#include "shm.h" + +#include <unistd.h> +#include <assert.h> + +#include <sys/types.h> +#include <sys/mman.h> +#include <linux/memfd.h> + +#include <tllist.h> + +#define LOG_MODULE "shm" +#include "log.h" +#include "stride.h" + +static tll(struct buffer) buffers; + +static void +buffer_destroy(struct buffer *buf) +{ + pixman_image_unref(buf->pix); + wl_buffer_destroy(buf->wl_buf); + munmap(buf->mmapped, buf->size); +} + +static void +buffer_release(void *data, struct wl_buffer *wl_buffer) +{ + //printf("buffer release\n"); + struct buffer *buffer = data; + assert(buffer->wl_buf == wl_buffer); + assert(buffer->busy); + buffer->busy = false; +} + +static const struct wl_buffer_listener buffer_listener = { + .release = &buffer_release, +}; + +struct buffer * +shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) +{ + /* Purge buffers marked for purging */ + tll_foreach(buffers, it) { + if (it->item.cookie != cookie) + continue; + + if (!it->item.purge) + continue; + + assert(!it->item.busy); + + LOG_DBG("cookie=%lx: purging buffer %p (width=%d, height=%d): %zu KB", + cookie, &it->item, it->item.width, it->item.height, + it->item.size / 1024); + + buffer_destroy(&it->item); + tll_remove(buffers, it); + } + + tll_foreach(buffers, it) { + if (!it->item.busy && + it->item.width == width && + it->item.height == height && + it->item.cookie == cookie) + { + it->item.busy = true; + it->item.purge = false; + return &it->item; + } + } + + /* Purge old buffers associated with this cookie */ + tll_foreach(buffers, it) { + if (it->item.cookie != cookie) + continue; + + if (it->item.busy) + continue; + + if (it->item.width == width && it->item.height == height) + continue; + + LOG_DBG("cookie=%lx: marking buffer %p for purging", cookie, &it->item); + it->item.purge = true; + } + + /* + * No existing buffer available. Create a new one by: + * + * 1. open a memory backed "file" with memfd_create() + * 2. mmap() the memory file, to be used by the pixman image + * 3. create a wayland shm buffer for the same memory file + * + * The pixman image and the wayland buffer are now sharing memory. + */ + + int pool_fd = -1; + void *mmapped = NULL; + size_t size = 0; + + struct wl_shm_pool *pool = NULL; + struct wl_buffer *buf = NULL; + pixman_image_t *pix = NULL; + + /* Backing memory for SHM */ + pool_fd = memfd_create("fuzzel-wayland-shm-buffer-pool", MFD_CLOEXEC); + if (pool_fd == -1) { + LOG_ERRNO("failed to create SHM backing memory file"); + goto err; + } + + /* Total size */ + const uint32_t stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); + size = stride * height; + if (ftruncate(pool_fd, size) == -1) { + LOG_ERRNO("failed to truncate SHM pool"); + goto err; + } + + mmapped = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, pool_fd, 0); + if (mmapped == MAP_FAILED) { + LOG_ERR("failed to mmap SHM backing memory file"); + goto err; + } + + pool = wl_shm_create_pool(shm, pool_fd, size); + if (pool == NULL) { + LOG_ERR("failed to create SHM pool"); + goto err; + } + + buf = wl_shm_pool_create_buffer( + pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); + if (buf == NULL) { + LOG_ERR("failed to create SHM buffer"); + goto err; + } + + /* We use the entire pool for our single buffer */ + wl_shm_pool_destroy(pool); pool = NULL; + close(pool_fd); pool_fd = -1; + + pix = pixman_image_create_bits_no_clear( + PIXMAN_a8r8g8b8, width, height, mmapped, stride); + if (pix == NULL) { + LOG_ERR("failed to create pixman image"); + goto err; + } + + /* Push to list of available buffers, but marked as 'busy' */ + tll_push_back( + buffers, + ((struct buffer){ + .width = width, + .height = height, + .stride = stride, + .cookie = cookie, + .busy = true, + .size = size, + .mmapped = mmapped, + .wl_buf = buf, + .pix = pix, + }) + ); + + struct buffer *ret = &tll_back(buffers); + wl_buffer_add_listener(ret->wl_buf, &buffer_listener, ret); + return ret; + +err: + if (pix != NULL) + pixman_image_unref(pix); + if (buf != NULL) + wl_buffer_destroy(buf); + if (pool != NULL) + wl_shm_pool_destroy(pool); + if (pool_fd != -1) + close(pool_fd); + if (mmapped != NULL) + munmap(mmapped, size); + + return NULL; +} + +void +shm_fini(void) +{ + tll_foreach(buffers, it) + buffer_destroy(&it->item); + tll_free(buffers); +} @@ -0,0 +1,25 @@ +#pragma once + +#include <stdbool.h> +#include <stddef.h> + +#include <pixman.h> +#include <wayland-client.h> + +struct buffer { + int width; + int height; + int stride; + unsigned long cookie; + + bool busy; + bool purge; + size_t size; + void *mmapped; + + struct wl_buffer *wl_buf; + pixman_image_t *pix; +}; + +struct buffer *shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie); +void shm_fini(void); diff --git a/stride.h b/stride.h new file mode 100644 index 0000000..b2c71a7 --- /dev/null +++ b/stride.h @@ -0,0 +1,9 @@ +#pragma once + +#include <pixman.h> + +static inline int +stride_for_format_and_width(pixman_format_code_t format, int width) +{ + return (((PIXMAN_FORMAT_BPP(format) * width + 7) / 8 + 4 - 1) & -4); +} |